From 55485e792c57ccce15de1b70905df943ae2207e9 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Mon, 18 Nov 2019 18:14:46 -0800 Subject: [PATCH 01/62] First work --- PowerShellEditorServices.sln | 77 +++- .../EditorServicesConfig.cs | 55 +++ .../EditorServicesHost.cs | 433 ++++++++++++++++++ .../EditorServicesLoader.cs | 57 +++ .../HostDetails.cs | 92 ++++ .../PowerShellEditorServices.Hosting.csproj | 29 ++ .../ProfilePaths.cs | 108 +++++ .../PsesLogLevel.cs | 16 + .../StartEditorServicesCommand.cs | 180 ++++++++ .../TransportConfig.cs | 155 +++++++ .../PowerShellEditorServices.VSCode.csproj | 1 + .../PowerShellEditorServices.csproj | 8 + 12 files changed, 1207 insertions(+), 4 deletions(-) create mode 100644 src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs create mode 100644 src/PowerShellEditorServices.Hosting/EditorServicesHost.cs create mode 100644 src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs create mode 100644 src/PowerShellEditorServices.Hosting/HostDetails.cs create mode 100644 src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj create mode 100644 src/PowerShellEditorServices.Hosting/ProfilePaths.cs create mode 100644 src/PowerShellEditorServices.Hosting/PsesLogLevel.cs create mode 100644 src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs create mode 100644 src/PowerShellEditorServices.Hosting/TransportConfig.cs diff --git a/PowerShellEditorServices.sln b/PowerShellEditorServices.sln index 30c954422..aab69e80e 100644 --- a/PowerShellEditorServices.sln +++ b/PowerShellEditorServices.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26430.12 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29505.145 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F594E7FD-1E72-4E51-A496-B019C2BA3180}" EndProject @@ -22,12 +22,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.VSCode", "src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj", "{3B38E8DA-8BFF-4264-AF16-47929E6398A3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices", "src\PowerShellEditorServices\PowerShellEditorServices.csproj", "{29EEDF03-0990-45F4-846E-2616970D1FA2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices", "src\PowerShellEditorServices\PowerShellEditorServices.csproj", "{29EEDF03-0990-45F4-846E-2616970D1FA2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerShellEditorServices.Test.E2E", "test\PowerShellEditorServices.Test.E2E\PowerShellEditorServices.Test.E2E.csproj", "{2561F253-8F72-436A-BCC3-AA63AB82EDC0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Test.E2E", "test\PowerShellEditorServices.Test.E2E\PowerShellEditorServices.Test.E2E.csproj", "{2561F253-8F72-436A-BCC3-AA63AB82EDC0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PowerShellEditorServices.Hosting", "src\PowerShellEditorServices.Hosting\PowerShellEditorServices.Hosting.csproj", "{3CC791E7-6FC9-4DDE-B4A2-547266977E4E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + CoreCLR|Any CPU = CoreCLR|Any CPU + CoreCLR|x64 = CoreCLR|x64 + CoreCLR|x86 = CoreCLR|x86 Debug|Any CPU = Debug|Any CPU Debug|x64 = Debug|x64 Debug|x86 = Debug|x86 @@ -36,6 +41,12 @@ Global Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|x64.Build.0 = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.CoreCLR|x86.Build.0 = Release|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|Any CPU.Build.0 = Debug|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -48,6 +59,12 @@ Global {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Release|x64.Build.0 = Release|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Release|x86.ActiveCfg = Release|Any CPU {3A5DDD20-5BD0-42F4-89F4-ACC0CE554028}.Release|x86.Build.0 = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|x64.Build.0 = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.CoreCLR|x86.Build.0 = Release|Any CPU {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -60,6 +77,12 @@ Global {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Release|x64.Build.0 = Release|Any CPU {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Release|x86.ActiveCfg = Release|Any CPU {8ED116F4-9DDF-4C49-AB96-AE462E3D64C3}.Release|x86.Build.0 = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|x64.Build.0 = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.CoreCLR|x86.Build.0 = Release|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Debug|Any CPU.Build.0 = Debug|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -72,6 +95,12 @@ Global {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|x64.Build.0 = Release|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|x86.ActiveCfg = Release|Any CPU {6A20B9E9-DE66-456E-B4F5-ACFD1A95C3CA}.Release|x86.Build.0 = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|x64.Build.0 = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.CoreCLR|x86.Build.0 = Release|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -84,6 +113,12 @@ Global {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Release|x64.Build.0 = Release|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Release|x86.ActiveCfg = Release|Any CPU {E3A5CF5D-6E41-44AC-AE0A-4C227E4BACD4}.Release|x86.Build.0 = Release|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|Any CPU.ActiveCfg = CoreCLR|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|Any CPU.Build.0 = CoreCLR|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|x64.ActiveCfg = CoreCLR|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|x64.Build.0 = CoreCLR|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|x86.ActiveCfg = CoreCLR|Any CPU + {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.CoreCLR|x86.Build.0 = CoreCLR|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Debug|Any CPU.Build.0 = Debug|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -96,6 +131,12 @@ Global {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x64.Build.0 = Release|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x86.ActiveCfg = Release|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x86.Build.0 = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|Any CPU.ActiveCfg = CoreCLR|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|Any CPU.Build.0 = CoreCLR|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x64.ActiveCfg = CoreCLR|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x64.Build.0 = CoreCLR|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x86.ActiveCfg = CoreCLR|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x86.Build.0 = CoreCLR|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -108,6 +149,12 @@ Global {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|x64.Build.0 = Release|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|x86.ActiveCfg = Release|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Release|x86.Build.0 = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|x64.Build.0 = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.CoreCLR|x86.Build.0 = Release|Any CPU {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|Any CPU.Build.0 = Debug|Any CPU {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -120,6 +167,24 @@ Global {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|x64.Build.0 = Release|Any CPU {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|x86.ActiveCfg = Release|Any CPU {2561F253-8F72-436A-BCC3-AA63AB82EDC0}.Release|x86.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|x64.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.CoreCLR|x86.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|x64.ActiveCfg = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|x64.Build.0 = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|x86.ActiveCfg = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Debug|x86.Build.0 = Debug|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|Any CPU.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|x64.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|x64.Build.0 = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|x86.ActiveCfg = Release|Any CPU + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -132,5 +197,9 @@ Global {3B38E8DA-8BFF-4264-AF16-47929E6398A3} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} {29EEDF03-0990-45F4-846E-2616970D1FA2} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} {2561F253-8F72-436A-BCC3-AA63AB82EDC0} = {422E561A-8118-4BE7-A54F-9309E4F03AAE} + {3CC791E7-6FC9-4DDE-B4A2-547266977E4E} = {F594E7FD-1E72-4E51-A496-B019C2BA3180} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3B9E8987-D4AC-426B-86F6-889126243A9A} EndGlobalSection EndGlobal diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs new file mode 100644 index 000000000..62d08b796 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs @@ -0,0 +1,55 @@ +using PowerShellEditorServices.Hosting; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + public enum ConsoleReplKind + { + None = 0, + LegacyReadLine, + PSReadLine, + } + + public class EditorServicesConfig + { + private ProfilePaths _profilePaths; + + public EditorServicesConfig( + HostDetails hostDetails, + string sessionDetailsPath, + string bundledModulePath, + string logPath) + { + HostDetails = hostDetails; + SessionDetailsPath = sessionDetailsPath; + BundledModulePath = bundledModulePath; + LogPath = logPath; + } + + public HostDetails HostDetails { get; } + + public string SessionDetailsPath { get; } + + public string BundledModulePath { get; } + + public string LogPath { get; } + + public ProfilePaths ProfilePaths { get; set; } = null; + + public IReadOnlyList AdditionalModules { get; set; } = null; + + public IReadOnlyList FeatureFlags { get; set; } = null; + + public ConsoleReplKind ConsoleRepl { get; set; } = ConsoleReplKind.None; + + public PsesLogLevel LogLevel { get; set; } = PsesLogLevel.Normal; + + public bool WaitForDebugger { get; set; } = false; + + public ITransportConfig LanguageServiceTransport { get; set; } = null; + + public ITransportConfig DebugServiceTransport { get; set; } = null; + } +} diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Hosting/EditorServicesHost.cs new file mode 100644 index 000000000..5806c8830 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/EditorServicesHost.cs @@ -0,0 +1,433 @@ +using System; +using System.Collections.Generic; +using System.Management.Automation.Host; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Provides a simplified interface for hosting the language and debug services + /// over the named pipe server protocol. + /// + internal class EditorServicesHost + { + #region Private Fields + + private readonly HostDetails _hostDetails; + + private readonly PSHost _internalHost; + + private readonly bool _enableConsoleRepl; + + private readonly bool _useLegacyReadLine; + + private readonly HashSet _featureFlags; + + private readonly string[] _additionalModules; + + private PsesLanguageServer _languageServer; + + private PsesDebugServer _debugServer; + + private Microsoft.Extensions.Logging.ILogger _logger; + + private ILoggerFactory _factory; + + #endregion + + #region Properties + + public EditorServicesHostStatus Status { get; private set; } + + #endregion + + #region Constructors + + /// + /// Initializes a new instance of the EditorServicesHost class and waits for + /// the debugger to attach if waitForDebugger is true. + /// + /// The details of the host which is launching PowerShell Editor Services. + /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. + /// If true, causes the host to wait for the debugger to attach before proceeding. + /// Modules to be loaded when initializing the new runspace. + /// Features to enable for this instance. + public EditorServicesHost( + HostDetails hostDetails, + string bundledModulesPath, + bool enableConsoleRepl, + bool useLegacyReadLine, + bool waitForDebugger, + string[] additionalModules, + string[] featureFlags) + : this( + hostDetails, + bundledModulesPath, + enableConsoleRepl, + useLegacyReadLine, + waitForDebugger, + additionalModules, + featureFlags, + GetInternalHostFromDefaultRunspace()) + { + } + + /// + /// Initializes a new instance of the EditorServicesHost class and waits for + /// the debugger to attach if waitForDebugger is true. + /// + /// The details of the host which is launching PowerShell Editor Services. + /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. + /// If true, causes the host to wait for the debugger to attach before proceeding. + /// Modules to be loaded when initializing the new runspace. + /// Features to enable for this instance. + /// The value of the $Host variable in the original runspace. + public EditorServicesHost( + HostDetails hostDetails, + string bundledModulesPath, + bool enableConsoleRepl, + bool useLegacyReadLine, + bool waitForDebugger, + string[] additionalModules, + string[] featureFlags, + PSHost internalHost) + { + // Validate.IsNotNull(nameof(hostDetails), hostDetails); + // Validate.IsNotNull(nameof(internalHost), internalHost); + + _hostDetails = hostDetails; + + _enableConsoleRepl = enableConsoleRepl; + _useLegacyReadLine = useLegacyReadLine; + _additionalModules = additionalModules ?? Array.Empty(); + _featureFlags = new HashSet(featureFlags ?? Array.Empty()); + _internalHost = internalHost; + +#if DEBUG + if (waitForDebugger) + { + if (System.Diagnostics.Debugger.IsAttached) + { + System.Diagnostics.Debugger.Break(); + } + else + { + System.Diagnostics.Debugger.Launch(); + } + } +#endif + + // Catch unhandled exceptions for logging purposes + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + } + + #endregion + + #region Public Methods + + /// + /// Starts the Logger for the specified file path and log level. + /// + /// The path of the log file to be written. + /// The minimum level of log messages to be written. + public void StartLogging(string logFilePath, PsesLogLevel logLevel) + { + } + + /// + /// Starts the language service with the specified config. + /// + /// The config that contains information on the communication protocol that will be used. + /// The profiles that will be loaded in the session. + public void StartLanguageService( + EditorServiceTransportConfig config, + ProfilePaths profilePaths) + { + _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); + + switch (config.TransportType) + { + case EditorServiceTransportType.NamedPipe: + _languageServer = new NamedPipePsesLanguageServer( + _factory, + LogLevel.Trace, + _enableConsoleRepl, + _useLegacyReadLine, + _featureFlags, + _hostDetails, + _additionalModules, + _internalHost, + profilePaths, + config.InOutPipeName ?? config.InPipeName, + config.OutPipeName); + break; + + case EditorServiceTransportType.Stdio: + _languageServer = new StdioPsesLanguageServer( + _factory, + LogLevel.Trace, + _featureFlags, + _hostDetails, + _additionalModules, + _internalHost, + profilePaths); + break; + } + + _logger.LogInformation("Starting language server"); + + Task.Run(_languageServer.StartAsync); + + _logger.LogInformation( + string.Format( + "Language service started, type = {0}, endpoint = {1}", + config.TransportType, config.Endpoint)); + } + + + private bool alreadySubscribedDebug; + /// + /// Starts the debug service with the specified config. + /// + /// The config that contains information on the communication protocol that will be used. + /// The profiles that will be loaded in the session. + /// Determines if we will make a new session typically used for temporary console debugging. + public void StartDebugService( + EditorServiceTransportConfig config, + ProfilePaths profilePaths, + bool useTempSession) + { + _logger.LogInformation($"Debug NamedPipe: {config.InOutPipeName}\nDebug OutPipe: {config.OutPipeName}"); + + IServiceProvider serviceProvider = null; + if (useTempSession) + { + serviceProvider = new ServiceCollection() + .AddLogging(builder => builder + .ClearProviders() + .AddSerilog() + .SetMinimumLevel(LogLevel.Trace)) + .AddSingleton(provider => null) + .AddPsesLanguageServices( + profilePaths, + _featureFlags, + _enableConsoleRepl, + _useLegacyReadLine, + _internalHost, + _hostDetails, + _additionalModules) + .BuildServiceProvider(); + } + + switch (config.TransportType) + { + case EditorServiceTransportType.NamedPipe: + NamedPipeServerStream inNamedPipe = CreateNamedPipe( + config.InOutPipeName ?? config.InPipeName, + config.OutPipeName, + out NamedPipeServerStream outNamedPipe); + + _debugServer = new PsesDebugServer( + _factory, + inNamedPipe, + outNamedPipe ?? inNamedPipe); + + Task[] tasks = outNamedPipe != null + ? new[] { inNamedPipe.WaitForConnectionAsync(), outNamedPipe.WaitForConnectionAsync() } + : new[] { inNamedPipe.WaitForConnectionAsync() }; + Task.WhenAll(tasks) + .ContinueWith(async task => + { + _logger.LogInformation("Starting debug server"); + await _debugServer.StartAsync(serviceProvider ?? _languageServer.LanguageServer.Services, useTempSession); + _logger.LogInformation( + $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); + }); + + break; + + case EditorServiceTransportType.Stdio: + _debugServer = new PsesDebugServer( + _factory, + Console.OpenStandardInput(), + Console.OpenStandardOutput()); + + _logger.LogInformation("Starting debug server"); + Task.Run(async () => + { + + await _debugServer.StartAsync(serviceProvider ?? _languageServer.LanguageServer.Services, useTempSession); + _logger.LogInformation( + $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); + }); + break; + + default: + throw new NotSupportedException($"The transport {config.TransportType} is not supported"); + } + + // If the instance of PSES is being used for debugging only, then we don't want to allow automatic restarting + // because the user can simply spin up a new PSES if they need to. + // This design decision was done since this "debug-only PSES" is used in the "Temporary Integrated Console debugging" + // feature which does not want PSES to be restarted so that the user can see the output of the last debug + // session. + if(!alreadySubscribedDebug && !useTempSession) + { + alreadySubscribedDebug = true; + _debugServer.SessionEnded += (sender, eventArgs) => + { + _debugServer.Dispose(); + alreadySubscribedDebug = false; + StartDebugService(config, profilePaths, useTempSession); + }; + } + } + + /// + /// Stops the language or debug services if either were started. + /// + public void StopServices() + { + // TODO: Need a new way to shut down the services + } + + /// + /// Waits for either the language or debug service to shut down. + /// + public void WaitForCompletion() + { + // If _languageServer is not null, then we are either using: + // Stdio - that only uses a LanguageServer so we return when that has shutdown. + // NamedPipes - that uses both LanguageServer and DebugServer, but LanguageServer + // is the core of PowerShell Editor Services and if that shuts down, + // we want the whole process to shutdown. + if (_languageServer != null) + { + _languageServer.WaitForShutdown().GetAwaiter().GetResult(); + return; + } + + // If there is no LanguageServer, then we must be running with the DebugServiceOnly switch + // (used in Temporary console debugging) and we need to wait for the DebugServer to shutdown. + _debugServer.WaitForShutdown().GetAwaiter().GetResult(); + } + + #endregion + + #region Private Methods + + private static PSHost GetInternalHostFromDefaultRunspace() + { + using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) + { + return pwsh.AddScript("$Host").Invoke().First(); + } + } + + /// + /// Gets the OSArchitecture for logging. Cannot use System.Runtime.InteropServices.RuntimeInformation.OSArchitecture + /// directly, since this tries to load API set DLLs in win7 and crashes. + /// + private string GetOSArchitecture() + { + // If on win7 (version 6.1.x), avoid System.Runtime.InteropServices.RuntimeInformation + if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version < new Version(6, 2)) + { + if (Environment.Is64BitProcess) + { + return "X64"; + } + + return "X86"; + } + + return RuntimeInformation.OSArchitecture.ToString(); + } + + private void CurrentDomain_UnhandledException( + object sender, + UnhandledExceptionEventArgs e) + { + // Log the exception + _logger.LogError($"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}"); + } + + 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 + }); + } + + #endregion + } +} + +} diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs new file mode 100644 index 000000000..149a9bbe4 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -0,0 +1,57 @@ +using Microsoft.PowerShell.EditorServices.Hosting; +using System; +using System.Collections.ObjectModel; +using System.IO; +using System.Management.Automation; +using System.Threading.Tasks; + +namespace PowerShellEditorServices.Hosting +{ + public class EditorServicesLoader : IDisposable + { + private const int Net461Version = 394254; + + private static readonly string s_dependencyPath = null; + + public static EditorServicesLoader Create(EditorServicesConfig hostConfig, string dependencyPath = null) + { + // TODO: Check host config transport configs + + if (hostConfig.ProfilePaths == null) + { + hostConfig.ProfilePaths = GetProfilePaths(hostConfig.HostDetails.ProfileId); + } + + return new EditorServicesLoader(hostConfig); + } + + private readonly EditorServicesConfig _hostConfig; + + public EditorServicesLoader(EditorServicesConfig hostConfig) + { + _hostConfig = hostConfig; + } + + public async Task LoadAndRunEditorServicesAsync() + { + } + + public void Dispose() + { + } + + private static ProfilePaths GetProfilePaths(string profileId) + { + Collection profileLocations = null; + using (var pwsh = PowerShell.Create()) + { + profileLocations = pwsh.AddScript("$profile.AllUsersAllHosts,$profile.CurrentUserAllHosts").Invoke(); + } + + return new ProfilePaths( + profileId, + baseAllUsersPath: Path.GetDirectoryName(profileLocations[0]), + baseCurrentUserPath: Path.GetDirectoryName(profileLocations[1])); + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/HostDetails.cs b/src/PowerShellEditorServices.Hosting/HostDetails.cs new file mode 100644 index 000000000..cdbe96f94 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/HostDetails.cs @@ -0,0 +1,92 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Contains details about the current host application (most + /// likely the editor which is using the host process). + /// + public class HostDetails + { + #region Constants + + /// + /// The default host name for PowerShell Editor Services. Used + /// if no host name is specified by the host application. + /// + public const string DefaultHostName = "PowerShell Editor Services Host"; + + /// + /// The default host ID for PowerShell Editor Services. Used + /// for the host-specific profile path if no host ID is specified. + /// + public const string DefaultHostProfileId = "Microsoft.PowerShellEditorServices"; + + /// + /// The default host version for PowerShell Editor Services. If + /// no version is specified by the host application, we use 0.0.0 + /// to indicate a lack of version. + /// + public static readonly Version DefaultHostVersion = new Version("0.0.0"); + + /// + /// The default host details in a HostDetails object. + /// + public static readonly HostDetails Default = new HostDetails(null, null, null); + + #endregion + + #region Properties + + /// + /// Gets the name of the host. + /// + public string Name { get; private set; } + + /// + /// Gets the profile ID of the host, used to determine the + /// host-specific profile path. + /// + public string ProfileId { get; private set; } + + /// + /// Gets the version of the host. + /// + public Version Version { get; private set; } + + #endregion + + #region Constructors + + /// + /// Creates an instance of the HostDetails class. + /// + /// + /// The display name for the host, typically in the form of + /// "[Application Name] Host". + /// + /// + /// The identifier of the PowerShell host to use for its profile path. + /// loaded. Used to resolve a profile path of the form 'X_profile.ps1' + /// where 'X' represents the value of hostProfileId. If null, a default + /// will be used. + /// + /// The host application's version. + public HostDetails( + string name, + string profileId, + Version version) + { + this.Name = name ?? DefaultHostName; + this.ProfileId = profileId ?? DefaultHostProfileId; + this.Version = version ?? DefaultHostVersion; + } + + #endregion + } +} diff --git a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj new file mode 100644 index 000000000..0963dfb10 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj @@ -0,0 +1,29 @@ + + + + netcoreapp2.1;net461 + + + + $(DefineConstants);CoreCLR + + + + + + + + + + + + + + + + + + + + + diff --git a/src/PowerShellEditorServices.Hosting/ProfilePaths.cs b/src/PowerShellEditorServices.Hosting/ProfilePaths.cs new file mode 100644 index 000000000..53eb714f1 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/ProfilePaths.cs @@ -0,0 +1,108 @@ +// +// 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.Linq; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + /// + /// Provides profile path resolution behavior relative to the name + /// of a particular PowerShell host. + /// + public class ProfilePaths + { + #region Constants + + /// + /// The file name for the "all hosts" profile. Also used as the + /// suffix for the host-specific profile filenames. + /// + public const string AllHostsProfileName = "profile.ps1"; + + #endregion + + #region Properties + + /// + /// Gets the profile path for all users, all hosts. + /// + public string AllUsersAllHosts { get; private set; } + + /// + /// Gets the profile path for all users, current host. + /// + public string AllUsersCurrentHost { get; private set; } + + /// + /// Gets the profile path for the current user, all hosts. + /// + public string CurrentUserAllHosts { get; private set; } + + /// + /// Gets the profile path for the current user and host. + /// + public string CurrentUserCurrentHost { get; private set; } + + #endregion + + #region Public Methods + + /// + /// Creates a new instance of the ProfilePaths class. + /// + /// + /// The identifier of the host used in the host-specific X_profile.ps1 filename. + /// + /// The base path to use for constructing AllUsers profile paths. + /// The base path to use for constructing CurrentUser profile paths. + public ProfilePaths( + string hostProfileId, + string baseAllUsersPath, + string baseCurrentUserPath) + { + this.Initialize(hostProfileId, baseAllUsersPath, baseCurrentUserPath); + } + + private void Initialize( + string hostProfileId, + string baseAllUsersPath, + string baseCurrentUserPath) + { + string currentHostProfileName = + string.Format( + "{0}_{1}", + hostProfileId, + AllHostsProfileName); + + this.AllUsersCurrentHost = Path.Combine(baseAllUsersPath, currentHostProfileName); + this.CurrentUserCurrentHost = Path.Combine(baseCurrentUserPath, currentHostProfileName); + this.AllUsersAllHosts = Path.Combine(baseAllUsersPath, AllHostsProfileName); + this.CurrentUserAllHosts = Path.Combine(baseCurrentUserPath, AllHostsProfileName); + } + + /// + /// Gets the list of profile paths that exist on the filesystem. + /// + /// An IEnumerable of profile path strings to be loaded. + public IEnumerable GetLoadableProfilePaths() + { + var profilePaths = + new string[] + { + this.AllUsersAllHosts, + this.AllUsersCurrentHost, + this.CurrentUserAllHosts, + this.CurrentUserCurrentHost + }; + + return profilePaths.Where(p => File.Exists(p)); + } + + #endregion + } +} + diff --git a/src/PowerShellEditorServices.Hosting/PsesLogLevel.cs b/src/PowerShellEditorServices.Hosting/PsesLogLevel.cs new file mode 100644 index 000000000..991188d0e --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/PsesLogLevel.cs @@ -0,0 +1,16 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + public enum PsesLogLevel + { + Diagnostic, + Verbose, + Normal, + Warning, + Error, + } +} diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs new file mode 100644 index 000000000..85b515926 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -0,0 +1,180 @@ +using Microsoft.PowerShell.Commands; +using PowerShellEditorServices.Hosting; +using System; +using System.Linq; +using System.Management.Automation; +using SMA = System.Management.Automation; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + [Cmdlet(VerbsLifecycle.Start, "EditorServices", DefaultParameterSetName = "NamedPipe")] + public sealed class StartEditorServicesCommand : PSCmdlet + { + /// + /// The name of the EditorServices host to report + /// + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string HostName { get; set; } + + /// + /// The ID to give to the host's profile. + /// + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string HostProfileId { get; set; } + + /// + /// The version to report for the host. + /// + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public Version HostVersion { get; set; } + + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string SessionDetailsPath { get; set; } + + [Parameter(ParameterSetName = "NamedPipe")] + public string LanguageServicePipeName { get; set; } + + [Parameter(ParameterSetName = "NamedPipe")] + public string DebugServicePipeName { get; set; } + + [Parameter(ParameterSetName = "NamedPipeSimplex")] + public string LanguageServiceInPipeName { get; set; } + + [Parameter(ParameterSetName = "NamedPipeSimplex")] + public string LanguageServiceOutPipeName { get; set; } + + [Parameter(ParameterSetName = "NamedPipeSimplex")] + public string DebugServiceInPipeName { get; set; } + + [Parameter(ParameterSetName = "NamedPipeSimplex")] + public string DebugServiceOutPipeName { get; set; } + + [Parameter(ParameterSetName = "Stdio")] + public SwitchParameter Stdio { get; set; } + + /// + /// The path to where PowerShellEditorServices and its bundled modules are. + /// + [Parameter] + [ValidateNotNullOrEmpty] + public string BundledModulePath { get; set; } + + [Parameter] + [ValidateNotNullOrEmpty] + public string LogPath { get; set; } + + [Parameter] + public PsesLogLevel LogLevel { get; set; } + + [Parameter] + public string[] AdditionalModules { get; set; } + + [Parameter] + public string[] FeatureFlags { get; set; } + + [Parameter] + public SwitchParameter EnableConsoleRepl { get; set; } + + [Parameter] + public SwitchParameter UseLegacyReadLine { get; set; } + + [Parameter] + public SwitchParameter DebugServiceOnly { get; set; } + + [Parameter] + public SwitchParameter WaitForDebugger { get; set; } + + protected override void EndProcessing() + { + using (var pwsh = SMA.PowerShell.Create()) + { + bool hasPSReadLine = pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Get-Module", typeof(GetModuleCommand))) + .AddParameter("Name", "PSReadLine") + .Invoke() + .Any(); + + if (hasPSReadLine) + { + pwsh.Commands.Clear(); + + pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Remove-Module", typeof(RemoveModuleCommand))) + .AddParameter("Name", "PSReadLine") + .AddParameter("ErrorAction", "SilentlyContinue"); + } + } + + var hostDetails = new HostDetails(HostName, HostProfileId, HostVersion); + var editorServicesConfig = new EditorServicesConfig(hostDetails, SessionDetailsPath, BundledModulePath, LogPath) + { + FeatureFlags = FeatureFlags, + LogLevel = LogLevel, + ConsoleRepl = GetReplKind(), + WaitForDebugger = WaitForDebugger, + AdditionalModules = AdditionalModules, + LanguageServiceTransport = GetLanguageServiceTransport(), + DebugServiceTransport = GetDebugServiceTransport(), + }; + + using (var psesLoader = EditorServicesLoader.Create(editorServicesConfig)) + { + psesLoader.LoadAndRunEditorServicesAsync().Wait(); + } + } + + private ConsoleReplKind GetReplKind() + { + if (Stdio || !EnableConsoleRepl) + { + return ConsoleReplKind.None; + } + + if (UseLegacyReadLine) + { + return ConsoleReplKind.LegacyReadLine; + } + + return ConsoleReplKind.PSReadLine; + } + + private ITransportConfig GetLanguageServiceTransport() + { + if (DebugServiceOnly) + { + return null; + } + + if (Stdio) + { + return new StdioTransportConfig(); + } + + if (LanguageServicePipeName != null) + { + return new DuplexNamedPipeTransportConfig(LanguageServicePipeName); + } + + return new SimplexNamedPipeTransportConfig(LanguageServiceInPipeName, LanguageServiceOutPipeName); + } + + private ITransportConfig GetDebugServiceTransport() + { + if (Stdio) + { + return DebugServiceOnly + ? new StdioTransportConfig() + : null; + } + + if (DebugServicePipeName != null) + { + return new DuplexNamedPipeTransportConfig(DebugServicePipeName); + } + + return new SimplexNamedPipeTransportConfig(DebugServiceInPipeName, DebugServiceOutPipeName); + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/TransportConfig.cs b/src/PowerShellEditorServices.Hosting/TransportConfig.cs new file mode 100644 index 000000000..860f89fc3 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/TransportConfig.cs @@ -0,0 +1,155 @@ +using System; +using System.IO; +using System.IO.Pipes; + +#if !CoreCLR +using System.Security.AccessControl; +using System.Security.Principal; +#endif + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + public enum TransportType + { + Stdio, + NamedPipe, + } + + public interface ITransportConfig + { + (Stream inStream, Stream outStream) CreateInOutStreams(); + + TransportType TransportType { get; } + + string Endpoint { get; } + + void Validate(); + } + + public class StdioTransportConfig : ITransportConfig + { + public TransportType TransportType => TransportType.Stdio; + + public string Endpoint => ""; + + public (Stream inStream, Stream outStream) CreateInOutStreams() + { + return (Console.OpenStandardInput(), Console.OpenStandardOutput()); + } + + public void Validate() + { + } + } + + public abstract class NamedPipeTransportConfig : ITransportConfig + { + private const int PipeBufferSize = 1024; + + public TransportType TransportType => TransportType.NamedPipe; + + public abstract string Endpoint { get; } + + public abstract (Stream inStream, Stream outStream) CreateInOutStreams(); + public abstract void Validate(); + + protected NamedPipeServerStream CreateNamedPipe(string pipeName, PipeDirection pipeDirection, PipeOptions extraPipeOptions = PipeOptions.None) + { +#if CoreCLR + return new NamedPipeServerStream( + pipeName: pipeName, + direction: pipeDirection, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.CurrentUserOnly | extraPipeOptions); +#else + + 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, domainSid: 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)); + } + + return new NamedPipeServerStream( + pipeName, + pipeDirection, + maxNumberOfServerInstances: 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous | extraPipeOptions, + inBufferSize: PipeBufferSize, + outBufferSize: PipeBufferSize, + pipeSecurity); +#endif + } + } + + public class DuplexNamedPipeTransportConfig : NamedPipeTransportConfig + { + private readonly string _pipeName; + + public DuplexNamedPipeTransportConfig(string pipeName) + { + _pipeName = pipeName; + } + public override string Endpoint => $"InOut pipe: {_pipeName}"; + + public override (Stream inStream, Stream outStream) CreateInOutStreams() + { + var extraPipeOptions = PipeOptions.None; +#if CoreCLR + extraPipeOptions |= PipeOptions.Asynchronous; +#endif + + NamedPipeServerStream namedPipe = CreateNamedPipe(_pipeName, PipeDirection.InOut, extraPipeOptions); + return (namedPipe, namedPipe); + } + + public override void Validate() + { + } + } + + public class SimplexNamedPipeTransportConfig : NamedPipeTransportConfig + { + private readonly string _inPipeName; + private readonly string _outPipeName; + + public SimplexNamedPipeTransportConfig(string inPipeName, string outPipeName) + { + _inPipeName = inPipeName; + _outPipeName = outPipeName; + } + + public override string Endpoint => $"In pipe: {_inPipeName} Out pipe: {_outPipeName}"; + + public override (Stream inStream, Stream outStream) CreateInOutStreams() + { + var extraInPipeOptions = PipeOptions.None; +#if CoreCLR + extraInPipeOptions |= PipeOptions.Asynchronous; +#endif + NamedPipeServerStream inPipe = CreateNamedPipe(_inPipeName, PipeDirection.InOut, extraInPipeOptions); + NamedPipeServerStream outPipe = CreateNamedPipe(_outPipeName, PipeDirection.Out); + + return (inPipe, outPipe); + } + + public override void Validate() + { + } + } +} diff --git a/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj b/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj index e1138cbe3..fc5fc85dd 100644 --- a/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj +++ b/src/PowerShellEditorServices.VSCode/PowerShellEditorServices.VSCode.csproj @@ -6,6 +6,7 @@ Provides added functionality to PowerShell Editor Services for the Visual Studio Code editor. netstandard2.0 Microsoft.PowerShell.EditorServices.VSCode + Debug;Release;CoreCLR diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index b7e1c6ce5..022814a8b 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -8,6 +8,7 @@ netstandard2.0 Microsoft.PowerShell.EditorServices Latest + Debug;Release;CoreCLR @@ -16,6 +17,13 @@ + + + latest + + + + latest From 57ce346c15b36d01f6a907b2c7a551c5a2d1623b Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 19 Nov 2019 20:08:50 -0800 Subject: [PATCH 02/62] More progress --- .../EditorServicesConfig.cs | 19 +- .../EditorServicesHost.cs | 433 -------------- .../EditorServicesLoader.cs | 36 +- .../EditorServicesRunner.cs | 224 ++++++++ .../HostDetails.cs | 92 --- .../HostInfo.cs | 22 + .../HostLogger.cs | 12 + .../NamedPipeUtils.cs | 100 ++++ .../PowerShellEditorServices.Hosting.csproj | 8 + .../ProfilePathConfig.cs | 19 + .../PsesLogLevel.cs | 16 - .../StartEditorServicesCommand.cs | 5 +- .../TransportConfig.cs | 104 +--- .../Hosting/EditorServicesHost.cs | 530 ------------------ .../Hosting/ProfilePaths.cs | 108 ---- .../Hosting/PsesLogLevel.cs | 45 -- .../Server/NamedPipePsesLanguageServer.cs | 155 ----- .../Server/PsesDebugServer.cs | 56 +- .../Server/PsesLanguageServer.cs | 45 +- .../Server/PsesServiceCollectionExtensions.cs | 10 +- .../Server/StdioPsesLanguageServer.cs | 44 -- .../PowerShellContextService.cs | 15 +- .../Session}/ProfilePaths.cs | 2 +- .../{Hosting/HostDetails.cs => hosting.cs} | 51 +- 24 files changed, 549 insertions(+), 1602 deletions(-) delete mode 100644 src/PowerShellEditorServices.Hosting/EditorServicesHost.cs create mode 100644 src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs delete mode 100644 src/PowerShellEditorServices.Hosting/HostDetails.cs create mode 100644 src/PowerShellEditorServices.Hosting/HostInfo.cs create mode 100644 src/PowerShellEditorServices.Hosting/HostLogger.cs create mode 100644 src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs create mode 100644 src/PowerShellEditorServices.Hosting/ProfilePathConfig.cs delete mode 100644 src/PowerShellEditorServices.Hosting/PsesLogLevel.cs delete mode 100644 src/PowerShellEditorServices/Hosting/EditorServicesHost.cs delete mode 100644 src/PowerShellEditorServices/Hosting/ProfilePaths.cs delete mode 100644 src/PowerShellEditorServices/Hosting/PsesLogLevel.cs delete mode 100644 src/PowerShellEditorServices/Server/NamedPipePsesLanguageServer.cs delete mode 100644 src/PowerShellEditorServices/Server/StdioPsesLanguageServer.cs rename src/{PowerShellEditorServices.Hosting => PowerShellEditorServices/Services/PowerShellContext/Session}/ProfilePaths.cs (98%) rename src/PowerShellEditorServices/{Hosting/HostDetails.cs => hosting.cs} (65%) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs index 62d08b796..7b4360540 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs @@ -1,6 +1,7 @@ using PowerShellEditorServices.Hosting; using System; using System.Collections.Generic; +using System.Management.Automation.Host; using System.Text; namespace Microsoft.PowerShell.EditorServices.Hosting @@ -14,21 +15,23 @@ public enum ConsoleReplKind public class EditorServicesConfig { - private ProfilePaths _profilePaths; - public EditorServicesConfig( - HostDetails hostDetails, + HostInfo hostInfo, + PSHost psHost, string sessionDetailsPath, string bundledModulePath, string logPath) { - HostDetails = hostDetails; + HostInfo = hostInfo; + PSHost = psHost; SessionDetailsPath = sessionDetailsPath; BundledModulePath = bundledModulePath; LogPath = logPath; } - public HostDetails HostDetails { get; } + public HostInfo HostInfo { get; } + + public PSHost PSHost { get; } public string SessionDetailsPath { get; } @@ -36,8 +39,6 @@ public EditorServicesConfig( public string LogPath { get; } - public ProfilePaths ProfilePaths { get; set; } = null; - public IReadOnlyList AdditionalModules { get; set; } = null; public IReadOnlyList FeatureFlags { get; set; } = null; @@ -46,10 +47,10 @@ public EditorServicesConfig( public PsesLogLevel LogLevel { get; set; } = PsesLogLevel.Normal; - public bool WaitForDebugger { get; set; } = false; - public ITransportConfig LanguageServiceTransport { get; set; } = null; public ITransportConfig DebugServiceTransport { get; set; } = null; + + public ProfilePathConfig ProfilePaths { get; set; } = null; } } diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices.Hosting/EditorServicesHost.cs deleted file mode 100644 index 5806c8830..000000000 --- a/src/PowerShellEditorServices.Hosting/EditorServicesHost.cs +++ /dev/null @@ -1,433 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Management.Automation.Host; - -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - /// - /// Provides a simplified interface for hosting the language and debug services - /// over the named pipe server protocol. - /// - internal class EditorServicesHost - { - #region Private Fields - - private readonly HostDetails _hostDetails; - - private readonly PSHost _internalHost; - - private readonly bool _enableConsoleRepl; - - private readonly bool _useLegacyReadLine; - - private readonly HashSet _featureFlags; - - private readonly string[] _additionalModules; - - private PsesLanguageServer _languageServer; - - private PsesDebugServer _debugServer; - - private Microsoft.Extensions.Logging.ILogger _logger; - - private ILoggerFactory _factory; - - #endregion - - #region Properties - - public EditorServicesHostStatus Status { get; private set; } - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the EditorServicesHost class and waits for - /// the debugger to attach if waitForDebugger is true. - /// - /// The details of the host which is launching PowerShell Editor Services. - /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. - /// If true, causes the host to wait for the debugger to attach before proceeding. - /// Modules to be loaded when initializing the new runspace. - /// Features to enable for this instance. - public EditorServicesHost( - HostDetails hostDetails, - string bundledModulesPath, - bool enableConsoleRepl, - bool useLegacyReadLine, - bool waitForDebugger, - string[] additionalModules, - string[] featureFlags) - : this( - hostDetails, - bundledModulesPath, - enableConsoleRepl, - useLegacyReadLine, - waitForDebugger, - additionalModules, - featureFlags, - GetInternalHostFromDefaultRunspace()) - { - } - - /// - /// Initializes a new instance of the EditorServicesHost class and waits for - /// the debugger to attach if waitForDebugger is true. - /// - /// The details of the host which is launching PowerShell Editor Services. - /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. - /// If true, causes the host to wait for the debugger to attach before proceeding. - /// Modules to be loaded when initializing the new runspace. - /// Features to enable for this instance. - /// The value of the $Host variable in the original runspace. - public EditorServicesHost( - HostDetails hostDetails, - string bundledModulesPath, - bool enableConsoleRepl, - bool useLegacyReadLine, - bool waitForDebugger, - string[] additionalModules, - string[] featureFlags, - PSHost internalHost) - { - // Validate.IsNotNull(nameof(hostDetails), hostDetails); - // Validate.IsNotNull(nameof(internalHost), internalHost); - - _hostDetails = hostDetails; - - _enableConsoleRepl = enableConsoleRepl; - _useLegacyReadLine = useLegacyReadLine; - _additionalModules = additionalModules ?? Array.Empty(); - _featureFlags = new HashSet(featureFlags ?? Array.Empty()); - _internalHost = internalHost; - -#if DEBUG - if (waitForDebugger) - { - if (System.Diagnostics.Debugger.IsAttached) - { - System.Diagnostics.Debugger.Break(); - } - else - { - System.Diagnostics.Debugger.Launch(); - } - } -#endif - - // Catch unhandled exceptions for logging purposes - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - } - - #endregion - - #region Public Methods - - /// - /// Starts the Logger for the specified file path and log level. - /// - /// The path of the log file to be written. - /// The minimum level of log messages to be written. - public void StartLogging(string logFilePath, PsesLogLevel logLevel) - { - } - - /// - /// Starts the language service with the specified config. - /// - /// The config that contains information on the communication protocol that will be used. - /// The profiles that will be loaded in the session. - public void StartLanguageService( - EditorServiceTransportConfig config, - ProfilePaths profilePaths) - { - _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); - - switch (config.TransportType) - { - case EditorServiceTransportType.NamedPipe: - _languageServer = new NamedPipePsesLanguageServer( - _factory, - LogLevel.Trace, - _enableConsoleRepl, - _useLegacyReadLine, - _featureFlags, - _hostDetails, - _additionalModules, - _internalHost, - profilePaths, - config.InOutPipeName ?? config.InPipeName, - config.OutPipeName); - break; - - case EditorServiceTransportType.Stdio: - _languageServer = new StdioPsesLanguageServer( - _factory, - LogLevel.Trace, - _featureFlags, - _hostDetails, - _additionalModules, - _internalHost, - profilePaths); - break; - } - - _logger.LogInformation("Starting language server"); - - Task.Run(_languageServer.StartAsync); - - _logger.LogInformation( - string.Format( - "Language service started, type = {0}, endpoint = {1}", - config.TransportType, config.Endpoint)); - } - - - private bool alreadySubscribedDebug; - /// - /// Starts the debug service with the specified config. - /// - /// The config that contains information on the communication protocol that will be used. - /// The profiles that will be loaded in the session. - /// Determines if we will make a new session typically used for temporary console debugging. - public void StartDebugService( - EditorServiceTransportConfig config, - ProfilePaths profilePaths, - bool useTempSession) - { - _logger.LogInformation($"Debug NamedPipe: {config.InOutPipeName}\nDebug OutPipe: {config.OutPipeName}"); - - IServiceProvider serviceProvider = null; - if (useTempSession) - { - serviceProvider = new ServiceCollection() - .AddLogging(builder => builder - .ClearProviders() - .AddSerilog() - .SetMinimumLevel(LogLevel.Trace)) - .AddSingleton(provider => null) - .AddPsesLanguageServices( - profilePaths, - _featureFlags, - _enableConsoleRepl, - _useLegacyReadLine, - _internalHost, - _hostDetails, - _additionalModules) - .BuildServiceProvider(); - } - - switch (config.TransportType) - { - case EditorServiceTransportType.NamedPipe: - NamedPipeServerStream inNamedPipe = CreateNamedPipe( - config.InOutPipeName ?? config.InPipeName, - config.OutPipeName, - out NamedPipeServerStream outNamedPipe); - - _debugServer = new PsesDebugServer( - _factory, - inNamedPipe, - outNamedPipe ?? inNamedPipe); - - Task[] tasks = outNamedPipe != null - ? new[] { inNamedPipe.WaitForConnectionAsync(), outNamedPipe.WaitForConnectionAsync() } - : new[] { inNamedPipe.WaitForConnectionAsync() }; - Task.WhenAll(tasks) - .ContinueWith(async task => - { - _logger.LogInformation("Starting debug server"); - await _debugServer.StartAsync(serviceProvider ?? _languageServer.LanguageServer.Services, useTempSession); - _logger.LogInformation( - $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); - }); - - break; - - case EditorServiceTransportType.Stdio: - _debugServer = new PsesDebugServer( - _factory, - Console.OpenStandardInput(), - Console.OpenStandardOutput()); - - _logger.LogInformation("Starting debug server"); - Task.Run(async () => - { - - await _debugServer.StartAsync(serviceProvider ?? _languageServer.LanguageServer.Services, useTempSession); - _logger.LogInformation( - $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); - }); - break; - - default: - throw new NotSupportedException($"The transport {config.TransportType} is not supported"); - } - - // If the instance of PSES is being used for debugging only, then we don't want to allow automatic restarting - // because the user can simply spin up a new PSES if they need to. - // This design decision was done since this "debug-only PSES" is used in the "Temporary Integrated Console debugging" - // feature which does not want PSES to be restarted so that the user can see the output of the last debug - // session. - if(!alreadySubscribedDebug && !useTempSession) - { - alreadySubscribedDebug = true; - _debugServer.SessionEnded += (sender, eventArgs) => - { - _debugServer.Dispose(); - alreadySubscribedDebug = false; - StartDebugService(config, profilePaths, useTempSession); - }; - } - } - - /// - /// Stops the language or debug services if either were started. - /// - public void StopServices() - { - // TODO: Need a new way to shut down the services - } - - /// - /// Waits for either the language or debug service to shut down. - /// - public void WaitForCompletion() - { - // If _languageServer is not null, then we are either using: - // Stdio - that only uses a LanguageServer so we return when that has shutdown. - // NamedPipes - that uses both LanguageServer and DebugServer, but LanguageServer - // is the core of PowerShell Editor Services and if that shuts down, - // we want the whole process to shutdown. - if (_languageServer != null) - { - _languageServer.WaitForShutdown().GetAwaiter().GetResult(); - return; - } - - // If there is no LanguageServer, then we must be running with the DebugServiceOnly switch - // (used in Temporary console debugging) and we need to wait for the DebugServer to shutdown. - _debugServer.WaitForShutdown().GetAwaiter().GetResult(); - } - - #endregion - - #region Private Methods - - private static PSHost GetInternalHostFromDefaultRunspace() - { - using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) - { - return pwsh.AddScript("$Host").Invoke().First(); - } - } - - /// - /// Gets the OSArchitecture for logging. Cannot use System.Runtime.InteropServices.RuntimeInformation.OSArchitecture - /// directly, since this tries to load API set DLLs in win7 and crashes. - /// - private string GetOSArchitecture() - { - // If on win7 (version 6.1.x), avoid System.Runtime.InteropServices.RuntimeInformation - if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version < new Version(6, 2)) - { - if (Environment.Is64BitProcess) - { - return "X64"; - } - - return "X86"; - } - - return RuntimeInformation.OSArchitecture.ToString(); - } - - private void CurrentDomain_UnhandledException( - object sender, - UnhandledExceptionEventArgs e) - { - // Log the exception - _logger.LogError($"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}"); - } - - 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 - }); - } - - #endregion - } -} - -} diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 149a9bbe4..77686ade8 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -1,4 +1,7 @@ -using Microsoft.PowerShell.EditorServices.Hosting; +using Microsoft.PowerShell.EditorServices; +using Microsoft.PowerShell.EditorServices.Hosting; +using Microsoft.PowerShell.EditorServices.Server; +using Serilog; using System; using System.Collections.ObjectModel; using System.IO; @@ -7,7 +10,7 @@ namespace PowerShellEditorServices.Hosting { - public class EditorServicesLoader : IDisposable + public sealed class EditorServicesLoader : IDisposable { private const int Net461Version = 394254; @@ -15,12 +18,7 @@ public class EditorServicesLoader : IDisposable public static EditorServicesLoader Create(EditorServicesConfig hostConfig, string dependencyPath = null) { - // TODO: Check host config transport configs - - if (hostConfig.ProfilePaths == null) - { - hostConfig.ProfilePaths = GetProfilePaths(hostConfig.HostDetails.ProfileId); - } + // TODO: Register assembly resolve event return new EditorServicesLoader(hostConfig); } @@ -34,24 +32,18 @@ public EditorServicesLoader(EditorServicesConfig hostConfig) public async Task LoadAndRunEditorServicesAsync() { - } - - public void Dispose() - { - } + // Method with no implementation that forces the PSES assembly to load, triggering an AssemblyResolve event + EditorServicesLoading.LoadEditorServicesForHost(); - private static ProfilePaths GetProfilePaths(string profileId) - { - Collection profileLocations = null; - using (var pwsh = PowerShell.Create()) + using (var editorServicesRunner = EditorServicesRunner.Create(_hostConfig)) { - profileLocations = pwsh.AddScript("$profile.AllUsersAllHosts,$profile.CurrentUserAllHosts").Invoke(); + await editorServicesRunner.RunUntilShutdown().ConfigureAwait(false); } + } - return new ProfilePaths( - profileId, - baseAllUsersPath: Path.GetDirectoryName(profileLocations[0]), - baseCurrentUserPath: Path.GetDirectoryName(profileLocations[1])); + public void Dispose() + { + // TODO: Deregister assembly event } } } diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs new file mode 100644 index 000000000..d440b4a79 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -0,0 +1,224 @@ +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Hosting; +using Microsoft.PowerShell.EditorServices.Server; +using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; +using Microsoft.Win32; +using Serilog; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.IO; +using System.Linq; +using System.Management.Automation; +using System.Text; +using System.Threading.Tasks; + +namespace PowerShellEditorServices.Hosting +{ + internal class EditorServicesRunner : IDisposable + { + public static EditorServicesRunner Create(EditorServicesConfig config) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.File(config.LogPath) + .MinimumLevel.Verbose() + .CreateLogger(); + + return new EditorServicesRunner(new LoggerFactory().AddSerilog(Log.Logger), config); + } + + private readonly ILoggerFactory _loggerFactory; + + private readonly Microsoft.Extensions.Logging.ILogger _logger; + + private readonly EditorServicesConfig _config; + + private bool _alreadySubscribedDebug; + + private EditorServicesRunner(ILoggerFactory loggerFactory, EditorServicesConfig config) + { + _loggerFactory = loggerFactory; + _logger = _loggerFactory.CreateLogger(); + _config = config; + _alreadySubscribedDebug = false; + } + + public async Task RunUntilShutdown() + { + bool creatingLanguageServer = _config.LanguageServiceTransport != null; + bool creatingDebugServer = _config.DebugServiceTransport != null; + bool isTempDebugSession = creatingDebugServer && !creatingLanguageServer; + + // TODO: Validate config here + + // Set up information required to instantiate servers + + ProfilePathConfig profilePaths = GetProfilePaths(_config.ProfilePaths); + + var hostDetails = new HostDetails( + _config.HostInfo.Name, + _config.HostInfo.ProfileId, + _config.HostInfo.Version, + _config.PSHost, + profilePaths.AllUsersProfilePath, + profilePaths.CurrentUserProfilePath, + consoleReplEnabled: _config.ConsoleRepl != ConsoleReplKind.None, + usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine); + + LogLevel minimumLogLevel = ConvertToExtensionLogLevel(_config.LogLevel); + + if (isTempDebugSession) + { + await RunTempDebugSession(hostDetails, minimumLogLevel).ConfigureAwait(false); + return; + } + + PsesLanguageServer languageServer = await CreateLanguageServer(hostDetails, minimumLogLevel).ConfigureAwait(false); + + Task debugServerCreation = null; + if (creatingDebugServer) + { + debugServerCreation = CreateDebugServerWithLanguageServer(languageServer.LanguageServer.Services); + } + + languageServer.StartAsync(); + + if (creatingDebugServer) + { + StartDebugServer(debugServerCreation); + } + + await languageServer.WaitForShutdown().ConfigureAwait(false); + return; + } + + public void Dispose() + { + _loggerFactory.Dispose(); + } + + private async Task RunTempDebugSession(HostDetails hostDetails, LogLevel minimumLogLevel) + { + PsesDebugServer debugServer = await CreateDebugServerForTempSession(hostDetails, minimumLogLevel).ConfigureAwait(false); + await debugServer.StartAsync().ConfigureAwait(false); + await debugServer.WaitForShutdown().ConfigureAwait(false); + return; + } + + private Task StartDebugServer(IServiceProvider serviceProvider) + { + Task debugServerCreation = CreateDebugServerWithLanguageServer(serviceProvider); + return StartDebugServer(debugServerCreation); + } + + private async Task StartDebugServer(Task debugServerCreation) + { + PsesDebugServer debugServer = await debugServerCreation.ConfigureAwait(false); + if (!_alreadySubscribedDebug) + { + _alreadySubscribedDebug = true; + debugServer.SessionEnded += DebugServer_OnSessionEnded; + } + debugServer.StartAsync(); + return; + } + + private async Task CreateLanguageServer(HostDetails hostDetails, LogLevel minimumLogLevel) + { + (Stream inStream, Stream outStream) = await _config.LanguageServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + + return new PsesLanguageServer( + _loggerFactory, + minimumLogLevel, + inStream, + outStream, + _config.FeatureFlags ?? Array.Empty(), + hostDetails, + _config.AdditionalModules ?? Array.Empty()); + } + + private async Task CreateDebugServerWithLanguageServer(IServiceProvider languageServerServiceProvider) + { + (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + + return PsesDebugServer.CreateWithLanguageServerServices(_loggerFactory, inStream, outStream, languageServerServiceProvider); + } + + private async Task CreateDebugServerForTempSession(HostDetails hostDetails, LogLevel minimumLogLevel) + { + (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + + return PsesDebugServer.CreateForTempSession(_loggerFactory, minimumLogLevel, inStream, outStream, _config.FeatureFlags, hostDetails, _config.AdditionalModules); + } + + private ProfilePathConfig GetProfilePaths(ProfilePathConfig profilePathConfig) + { + string allUsersPath = null; + string currentUserPath = null; + + if (profilePathConfig != null) + { + allUsersPath = profilePathConfig.AllUsersProfilePath; + currentUserPath = profilePathConfig.CurrentUserProfilePath; + } + + if (allUsersPath == null || currentUserPath == null) + { + using (var pwsh = PowerShell.Create()) + { + Collection profiles = pwsh.AddScript("$profile.AllUsersAllHosts,$profile.CurrentUserAllHosts") + .Invoke(); + + if (allUsersPath == null) + { + allUsersPath = profiles[0]; + } + + if (currentUserPath == null) + { + currentUserPath = profiles[1]; + } + } + } + + return new ProfilePathConfig(allUsersPath, currentUserPath); + } + + private void DebugServer_OnSessionEnded(object sender, EventArgs args) + { + var oldServer = (PsesDebugServer)sender; + IServiceProvider serviceProvider = oldServer.ServiceProvider; + oldServer.Dispose(); + _alreadySubscribedDebug = false; + Task.Run(() => + { + StartDebugServer(serviceProvider); + }); + } + + private static LogLevel ConvertToExtensionLogLevel(PsesLogLevel logLevel) + { + switch (logLevel) + { + case PsesLogLevel.Diagnostic: + return LogLevel.Trace; + + case PsesLogLevel.Verbose: + return LogLevel.Debug; + + case PsesLogLevel.Normal: + return LogLevel.Information; + + case PsesLogLevel.Warning: + return LogLevel.Warning; + + case PsesLogLevel.Error: + return LogLevel.Error; + + default: + return LogLevel.Information; + } + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/HostDetails.cs b/src/PowerShellEditorServices.Hosting/HostDetails.cs deleted file mode 100644 index cdbe96f94..000000000 --- a/src/PowerShellEditorServices.Hosting/HostDetails.cs +++ /dev/null @@ -1,92 +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; - -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - /// - /// Contains details about the current host application (most - /// likely the editor which is using the host process). - /// - public class HostDetails - { - #region Constants - - /// - /// The default host name for PowerShell Editor Services. Used - /// if no host name is specified by the host application. - /// - public const string DefaultHostName = "PowerShell Editor Services Host"; - - /// - /// The default host ID for PowerShell Editor Services. Used - /// for the host-specific profile path if no host ID is specified. - /// - public const string DefaultHostProfileId = "Microsoft.PowerShellEditorServices"; - - /// - /// The default host version for PowerShell Editor Services. If - /// no version is specified by the host application, we use 0.0.0 - /// to indicate a lack of version. - /// - public static readonly Version DefaultHostVersion = new Version("0.0.0"); - - /// - /// The default host details in a HostDetails object. - /// - public static readonly HostDetails Default = new HostDetails(null, null, null); - - #endregion - - #region Properties - - /// - /// Gets the name of the host. - /// - public string Name { get; private set; } - - /// - /// Gets the profile ID of the host, used to determine the - /// host-specific profile path. - /// - public string ProfileId { get; private set; } - - /// - /// Gets the version of the host. - /// - public Version Version { get; private set; } - - #endregion - - #region Constructors - - /// - /// Creates an instance of the HostDetails class. - /// - /// - /// The display name for the host, typically in the form of - /// "[Application Name] Host". - /// - /// - /// The identifier of the PowerShell host to use for its profile path. - /// loaded. Used to resolve a profile path of the form 'X_profile.ps1' - /// where 'X' represents the value of hostProfileId. If null, a default - /// will be used. - /// - /// The host application's version. - public HostDetails( - string name, - string profileId, - Version version) - { - this.Name = name ?? DefaultHostName; - this.ProfileId = profileId ?? DefaultHostProfileId; - this.Version = version ?? DefaultHostVersion; - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices.Hosting/HostInfo.cs b/src/PowerShellEditorServices.Hosting/HostInfo.cs new file mode 100644 index 000000000..b8f647a08 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/HostInfo.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + public class HostInfo + { + public HostInfo(string name, string profileId, Version version) + { + Name = name; + ProfileId = profileId; + Version = version; + } + + public string Name { get; } + + public string ProfileId { get; } + + public Version Version { get; } + } +} diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs new file mode 100644 index 000000000..200de22da --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -0,0 +1,12 @@ + +namespace PowerShellEditorServices.Hosting +{ + public enum PsesLogLevel + { + Diagnostic, + Verbose, + Normal, + Warning, + Error, + } +} diff --git a/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs b/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs new file mode 100644 index 000000000..fcf82da0c --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Security.Principal; +using System.Text; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + internal static class NamedPipeUtils + { + private const int PipeBufferSize = 1024; + + internal static NamedPipeServerStream CreateNamedPipe( + string pipeName, + PipeDirection pipeDirection) + { +#if CoreCLR + return new NamedPipeServerStream( + pipeName: pipeName, + direction: pipeDirection, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); +#else + + 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, domainSid: 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)); + } + + return new NamedPipeServerStream( + pipeName, + pipeDirection, + maxNumberOfServerInstances: 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + inBufferSize: PipeBufferSize, + outBufferSize: PipeBufferSize, + pipeSecurity); +#endif + } + + public static string GenerateValidNamedPipeName() + { + int tries = 0; + do + { + string pipeName = $"PSES_{Path.GetRandomFileName()}"; + + if (IsPipeNameValid(pipeName)) + { + return pipeName; + } + + } while (tries < 10); + + throw new Exception("Unable to create named pipe; no available names"); + } + + public static bool IsPipeNameValid(string pipeName) + { + if (string.IsNullOrEmpty(pipeName)) + { + return false; + } + + return !File.Exists(GetNamedPipePath(pipeName)); + } + + public static string GetNamedPipePath(string pipeName) + { +#if CoreCLR + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return Path.Combine(Path.GetTempPath(), $"CoreFxPipe_{pipeName}"); + } +#endif + + return $@"\\.\pipe\{pipeName}"; + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj index 0963dfb10..da3a6d099 100644 --- a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj +++ b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj @@ -2,6 +2,7 @@ netcoreapp2.1;net461 + Microsoft.PowerShell.EditorServices.Hosting @@ -10,6 +11,9 @@ + + ..\..\..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\Microsoft.NET.Build.Extensions\net461\lib\System.Runtime.InteropServices.RuntimeInformation.dll + @@ -22,6 +26,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/PowerShellEditorServices.Hosting/ProfilePathConfig.cs b/src/PowerShellEditorServices.Hosting/ProfilePathConfig.cs new file mode 100644 index 000000000..e4faaffa5 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/ProfilePathConfig.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace PowerShellEditorServices.Hosting +{ + public class ProfilePathConfig + { + public ProfilePathConfig(string allUsersPath, string currentUserPath) + { + AllUsersProfilePath = allUsersPath; + CurrentUserProfilePath = currentUserPath; + } + + public string AllUsersProfilePath { get; } + + public string CurrentUserProfilePath { get; } + } +} diff --git a/src/PowerShellEditorServices.Hosting/PsesLogLevel.cs b/src/PowerShellEditorServices.Hosting/PsesLogLevel.cs deleted file mode 100644 index 991188d0e..000000000 --- a/src/PowerShellEditorServices.Hosting/PsesLogLevel.cs +++ /dev/null @@ -1,16 +0,0 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// - -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - public enum PsesLogLevel - { - Diagnostic, - Verbose, - Normal, - Warning, - Error, - } -} diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 85b515926..a69589fd5 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -107,13 +107,12 @@ protected override void EndProcessing() } } - var hostDetails = new HostDetails(HostName, HostProfileId, HostVersion); - var editorServicesConfig = new EditorServicesConfig(hostDetails, SessionDetailsPath, BundledModulePath, LogPath) + var hostInfo = new HostInfo(HostName, HostProfileId, HostVersion); + var editorServicesConfig = new EditorServicesConfig(hostInfo, Host, SessionDetailsPath, BundledModulePath, LogPath) { FeatureFlags = FeatureFlags, LogLevel = LogLevel, ConsoleRepl = GetReplKind(), - WaitForDebugger = WaitForDebugger, AdditionalModules = AdditionalModules, LanguageServiceTransport = GetLanguageServiceTransport(), DebugServiceTransport = GetDebugServiceTransport(), diff --git a/src/PowerShellEditorServices.Hosting/TransportConfig.cs b/src/PowerShellEditorServices.Hosting/TransportConfig.cs index 860f89fc3..8e6c7e6cb 100644 --- a/src/PowerShellEditorServices.Hosting/TransportConfig.cs +++ b/src/PowerShellEditorServices.Hosting/TransportConfig.cs @@ -5,6 +5,7 @@ #if !CoreCLR using System.Security.AccessControl; using System.Security.Principal; +using System.Threading.Tasks; #endif namespace Microsoft.PowerShell.EditorServices.Hosting @@ -17,7 +18,7 @@ public enum TransportType public interface ITransportConfig { - (Stream inStream, Stream outStream) CreateInOutStreams(); + Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync(); TransportType TransportType { get; } @@ -32,9 +33,9 @@ public class StdioTransportConfig : ITransportConfig public string Endpoint => ""; - public (Stream inStream, Stream outStream) CreateInOutStreams() + public Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync() { - return (Console.OpenStandardInput(), Console.OpenStandardOutput()); + return Task.FromResult((Console.OpenStandardInput(), Console.OpenStandardOutput())); } public void Validate() @@ -42,62 +43,7 @@ public void Validate() } } - public abstract class NamedPipeTransportConfig : ITransportConfig - { - private const int PipeBufferSize = 1024; - - public TransportType TransportType => TransportType.NamedPipe; - - public abstract string Endpoint { get; } - - public abstract (Stream inStream, Stream outStream) CreateInOutStreams(); - public abstract void Validate(); - - protected NamedPipeServerStream CreateNamedPipe(string pipeName, PipeDirection pipeDirection, PipeOptions extraPipeOptions = PipeOptions.None) - { -#if CoreCLR - return new NamedPipeServerStream( - pipeName: pipeName, - direction: pipeDirection, - maxNumberOfServerInstances: 1, - transmissionMode: PipeTransmissionMode.Byte, - options: PipeOptions.CurrentUserOnly | extraPipeOptions); -#else - - 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, domainSid: 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)); - } - - return new NamedPipeServerStream( - pipeName, - pipeDirection, - maxNumberOfServerInstances: 1, - PipeTransmissionMode.Byte, - PipeOptions.Asynchronous | extraPipeOptions, - inBufferSize: PipeBufferSize, - outBufferSize: PipeBufferSize, - pipeSecurity); -#endif - } - } - - public class DuplexNamedPipeTransportConfig : NamedPipeTransportConfig + public class DuplexNamedPipeTransportConfig : ITransportConfig { private readonly string _pipeName; @@ -105,25 +51,24 @@ public DuplexNamedPipeTransportConfig(string pipeName) { _pipeName = pipeName; } - public override string Endpoint => $"InOut pipe: {_pipeName}"; - public override (Stream inStream, Stream outStream) CreateInOutStreams() - { - var extraPipeOptions = PipeOptions.None; -#if CoreCLR - extraPipeOptions |= PipeOptions.Asynchronous; -#endif + public string Endpoint => $"InOut pipe: {_pipeName}"; + + public TransportType TransportType => TransportType.NamedPipe; - NamedPipeServerStream namedPipe = CreateNamedPipe(_pipeName, PipeDirection.InOut, extraPipeOptions); + public async Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync() + { + NamedPipeServerStream namedPipe = NamedPipeUtils.CreateNamedPipe(_pipeName, PipeDirection.InOut); + await namedPipe.WaitForConnectionAsync().ConfigureAwait(false); return (namedPipe, namedPipe); } - public override void Validate() + public void Validate() { } } - public class SimplexNamedPipeTransportConfig : NamedPipeTransportConfig + public class SimplexNamedPipeTransportConfig : ITransportConfig { private readonly string _inPipeName; private readonly string _outPipeName; @@ -134,21 +79,24 @@ public SimplexNamedPipeTransportConfig(string inPipeName, string outPipeName) _outPipeName = outPipeName; } - public override string Endpoint => $"In pipe: {_inPipeName} Out pipe: {_outPipeName}"; + public string Endpoint => $"In pipe: {_inPipeName} Out pipe: {_outPipeName}"; - public override (Stream inStream, Stream outStream) CreateInOutStreams() + public TransportType TransportType => TransportType.NamedPipe; + + public async Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync() { - var extraInPipeOptions = PipeOptions.None; -#if CoreCLR - extraInPipeOptions |= PipeOptions.Asynchronous; -#endif - NamedPipeServerStream inPipe = CreateNamedPipe(_inPipeName, PipeDirection.InOut, extraInPipeOptions); - NamedPipeServerStream outPipe = CreateNamedPipe(_outPipeName, PipeDirection.Out); + NamedPipeServerStream inPipe = NamedPipeUtils.CreateNamedPipe(_inPipeName, PipeDirection.InOut); + Task inPipeConnected = inPipe.WaitForConnectionAsync(); + + NamedPipeServerStream outPipe = NamedPipeUtils.CreateNamedPipe(_outPipeName, PipeDirection.Out); + Task outPipeConnected = outPipe.WaitForConnectionAsync(); + + await Task.WhenAll(inPipeConnected, outPipeConnected).ConfigureAwait(false); return (inPipe, outPipe); } - public override void Validate() + public void Validate() { } } diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs b/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs deleted file mode 100644 index eed6555fa..000000000 --- a/src/PowerShellEditorServices/Hosting/EditorServicesHost.cs +++ /dev/null @@ -1,530 +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; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO.Pipes; -using System.Linq; -using System.Management.Automation; -using System.Management.Automation.Host; -using System.Reflection; -using System.Runtime.InteropServices; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Server; -using Microsoft.PowerShell.EditorServices.Services; -using Microsoft.PowerShell.EditorServices.Utility; -using OmniSharp.Extensions.LanguageServer.Protocol.Server; -using Serilog; - -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - public enum EditorServicesHostStatus - { - Started, - Failed, - Ended - } - - public enum EditorServiceTransportType - { - NamedPipe, - Stdio - } - - public class EditorServiceTransportConfig - { - public EditorServiceTransportType TransportType { get; set; } - /// - /// Configures the endpoint of the transport. - /// For Stdio it's ignored. - /// For NamedPipe it's the pipe name. - /// - public string InOutPipeName { get; set; } - - public string OutPipeName { get; set; } - - public string InPipeName { get; set; } - - internal string Endpoint => OutPipeName != null && InPipeName != null ? $"In pipe: {InPipeName} Out pipe: {OutPipeName}" : $" InOut pipe: {InOutPipeName}"; - } - - /// - /// Provides a simplified interface for hosting the language and debug services - /// over the named pipe server protocol. - /// - public class EditorServicesHost - { - #region Private Fields - - // 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 HostDetails _hostDetails; - - private readonly PSHost _internalHost; - - private readonly bool _enableConsoleRepl; - - private readonly bool _useLegacyReadLine; - - private readonly HashSet _featureFlags; - - private readonly string[] _additionalModules; - - private PsesLanguageServer _languageServer; - private PsesDebugServer _debugServer; - - private Microsoft.Extensions.Logging.ILogger _logger; - - private ILoggerFactory _factory; - - #endregion - - #region Properties - - public EditorServicesHostStatus Status { get; private set; } - - #endregion - - #region Constructors - - /// - /// Initializes a new instance of the EditorServicesHost class and waits for - /// the debugger to attach if waitForDebugger is true. - /// - /// The details of the host which is launching PowerShell Editor Services. - /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. - /// If true, causes the host to wait for the debugger to attach before proceeding. - /// Modules to be loaded when initializing the new runspace. - /// Features to enable for this instance. - public EditorServicesHost( - HostDetails hostDetails, - string bundledModulesPath, - bool enableConsoleRepl, - bool useLegacyReadLine, - bool waitForDebugger, - string[] additionalModules, - string[] featureFlags) - : this( - hostDetails, - bundledModulesPath, - enableConsoleRepl, - useLegacyReadLine, - waitForDebugger, - additionalModules, - featureFlags, - GetInternalHostFromDefaultRunspace()) - { - } - - /// - /// Initializes a new instance of the EditorServicesHost class and waits for - /// the debugger to attach if waitForDebugger is true. - /// - /// The details of the host which is launching PowerShell Editor Services. - /// Provides a path to PowerShell modules bundled with the host, if any. Null otherwise. - /// If true, causes the host to wait for the debugger to attach before proceeding. - /// Modules to be loaded when initializing the new runspace. - /// Features to enable for this instance. - /// The value of the $Host variable in the original runspace. - public EditorServicesHost( - HostDetails hostDetails, - string bundledModulesPath, - bool enableConsoleRepl, - bool useLegacyReadLine, - bool waitForDebugger, - string[] additionalModules, - string[] featureFlags, - PSHost internalHost) - { - Validate.IsNotNull(nameof(hostDetails), hostDetails); - Validate.IsNotNull(nameof(internalHost), internalHost); - - _hostDetails = hostDetails; - - _enableConsoleRepl = enableConsoleRepl; - _useLegacyReadLine = useLegacyReadLine; - _additionalModules = additionalModules ?? Array.Empty(); - _featureFlags = new HashSet(featureFlags ?? Array.Empty()); - _internalHost = internalHost; - -#if DEBUG - if (waitForDebugger) - { - if (System.Diagnostics.Debugger.IsAttached) - { - System.Diagnostics.Debugger.Break(); - } - else - { - System.Diagnostics.Debugger.Launch(); - } - } -#endif - - // Catch unhandled exceptions for logging purposes - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; - } - - #endregion - - #region Public Methods - - /// - /// Starts the Logger for the specified file path and log level. - /// - /// The path of the log file to be written. - /// The minimum level of log messages to be written. - public void StartLogging(string logFilePath, PsesLogLevel logLevel) - { - Log.Logger = new LoggerConfiguration().Enrich.FromLogContext() - .WriteTo.File(logFilePath) - .MinimumLevel.Verbose() - .CreateLogger(); - _factory = new LoggerFactory().AddSerilog(Log.Logger); - _logger = _factory.CreateLogger(); - - FileVersionInfo fileVersionInfo = - FileVersionInfo.GetVersionInfo(this.GetType().GetTypeInfo().Assembly.Location); - - string osVersion = RuntimeInformation.OSDescription; - - string osArch = GetOSArchitecture(); - - string buildTime = BuildInfo.BuildTime?.ToString("s", System.Globalization.CultureInfo.InvariantCulture) ?? ""; - - string logHeader = $@" -PowerShell Editor Services Host v{fileVersionInfo.FileVersion} starting (PID {Process.GetCurrentProcess().Id}) - - Host application details: - - Name: {_hostDetails.Name} - Version: {_hostDetails.Version} - ProfileId: {_hostDetails.ProfileId} - Arch: {osArch} - - Operating system details: - - Version: {osVersion} - Arch: {osArch} - - Build information: - - Version: {BuildInfo.BuildVersion} - Origin: {BuildInfo.BuildOrigin} - Date: {buildTime} -"; - - _logger.LogInformation(logHeader); - } - - /// - /// Starts the language service with the specified config. - /// - /// The config that contains information on the communication protocol that will be used. - /// The profiles that will be loaded in the session. - public void StartLanguageService( - EditorServiceTransportConfig config, - ProfilePaths profilePaths) - { - _logger.LogInformation($"LSP NamedPipe: {config.InOutPipeName}\nLSP OutPipe: {config.OutPipeName}"); - - switch (config.TransportType) - { - case EditorServiceTransportType.NamedPipe: - _languageServer = new NamedPipePsesLanguageServer( - _factory, - LogLevel.Trace, - _enableConsoleRepl, - _useLegacyReadLine, - _featureFlags, - _hostDetails, - _additionalModules, - _internalHost, - profilePaths, - config.InOutPipeName ?? config.InPipeName, - config.OutPipeName); - break; - - case EditorServiceTransportType.Stdio: - _languageServer = new StdioPsesLanguageServer( - _factory, - LogLevel.Trace, - _featureFlags, - _hostDetails, - _additionalModules, - _internalHost, - profilePaths); - break; - } - - _logger.LogInformation("Starting language server"); - - Task.Run(_languageServer.StartAsync); - - _logger.LogInformation( - string.Format( - "Language service started, type = {0}, endpoint = {1}", - config.TransportType, config.Endpoint)); - } - - - private bool alreadySubscribedDebug; - /// - /// Starts the debug service with the specified config. - /// - /// The config that contains information on the communication protocol that will be used. - /// The profiles that will be loaded in the session. - /// Determines if we will make a new session typically used for temporary console debugging. - public void StartDebugService( - EditorServiceTransportConfig config, - ProfilePaths profilePaths, - bool useTempSession) - { - _logger.LogInformation($"Debug NamedPipe: {config.InOutPipeName}\nDebug OutPipe: {config.OutPipeName}"); - - IServiceProvider serviceProvider = null; - if (useTempSession) - { - serviceProvider = new ServiceCollection() - .AddLogging(builder => builder - .ClearProviders() - .AddSerilog() - .SetMinimumLevel(LogLevel.Trace)) - .AddSingleton(provider => null) - .AddPsesLanguageServices( - profilePaths, - _featureFlags, - _enableConsoleRepl, - _useLegacyReadLine, - _internalHost, - _hostDetails, - _additionalModules) - .BuildServiceProvider(); - } - - switch (config.TransportType) - { - case EditorServiceTransportType.NamedPipe: - NamedPipeServerStream inNamedPipe = CreateNamedPipe( - config.InOutPipeName ?? config.InPipeName, - config.OutPipeName, - out NamedPipeServerStream outNamedPipe); - - _debugServer = new PsesDebugServer( - _factory, - inNamedPipe, - outNamedPipe ?? inNamedPipe); - - Task[] tasks = outNamedPipe != null - ? new[] { inNamedPipe.WaitForConnectionAsync(), outNamedPipe.WaitForConnectionAsync() } - : new[] { inNamedPipe.WaitForConnectionAsync() }; - Task.WhenAll(tasks) - .ContinueWith(async task => - { - _logger.LogInformation("Starting debug server"); - await _debugServer.StartAsync(serviceProvider ?? _languageServer.LanguageServer.Services, useTempSession); - _logger.LogInformation( - $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); - }); - - break; - - case EditorServiceTransportType.Stdio: - _debugServer = new PsesDebugServer( - _factory, - Console.OpenStandardInput(), - Console.OpenStandardOutput()); - - _logger.LogInformation("Starting debug server"); - Task.Run(async () => - { - - await _debugServer.StartAsync(serviceProvider ?? _languageServer.LanguageServer.Services, useTempSession); - _logger.LogInformation( - $"Debug service started, type = {config.TransportType}, endpoint = {config.Endpoint}"); - }); - break; - - default: - throw new NotSupportedException($"The transport {config.TransportType} is not supported"); - } - - // If the instance of PSES is being used for debugging only, then we don't want to allow automatic restarting - // because the user can simply spin up a new PSES if they need to. - // This design decision was done since this "debug-only PSES" is used in the "Temporary Integrated Console debugging" - // feature which does not want PSES to be restarted so that the user can see the output of the last debug - // session. - if(!alreadySubscribedDebug && !useTempSession) - { - alreadySubscribedDebug = true; - _debugServer.SessionEnded += (sender, eventArgs) => - { - _debugServer.Dispose(); - alreadySubscribedDebug = false; - StartDebugService(config, profilePaths, useTempSession); - }; - } - } - - /// - /// Stops the language or debug services if either were started. - /// - public void StopServices() - { - // TODO: Need a new way to shut down the services - } - - /// - /// Waits for either the language or debug service to shut down. - /// - public void WaitForCompletion() - { - // If _languageServer is not null, then we are either using: - // Stdio - that only uses a LanguageServer so we return when that has shutdown. - // NamedPipes - that uses both LanguageServer and DebugServer, but LanguageServer - // is the core of PowerShell Editor Services and if that shuts down, - // we want the whole process to shutdown. - if (_languageServer != null) - { - _languageServer.WaitForShutdown().GetAwaiter().GetResult(); - return; - } - - // If there is no LanguageServer, then we must be running with the DebugServiceOnly switch - // (used in Temporary console debugging) and we need to wait for the DebugServer to shutdown. - _debugServer.WaitForShutdown().GetAwaiter().GetResult(); - } - - #endregion - - #region Private Methods - - private static PSHost GetInternalHostFromDefaultRunspace() - { - using (var pwsh = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) - { - return pwsh.AddScript("$Host").Invoke().First(); - } - } - - /// - /// Gets the OSArchitecture for logging. Cannot use System.Runtime.InteropServices.RuntimeInformation.OSArchitecture - /// directly, since this tries to load API set DLLs in win7 and crashes. - /// - private string GetOSArchitecture() - { - // If on win7 (version 6.1.x), avoid System.Runtime.InteropServices.RuntimeInformation - if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version < new Version(6, 2)) - { - if (Environment.Is64BitProcess) - { - return "X64"; - } - - return "X86"; - } - - return RuntimeInformation.OSArchitecture.ToString(); - } - - private void CurrentDomain_UnhandledException( - object sender, - UnhandledExceptionEventArgs e) - { - // Log the exception - _logger.LogError($"FATAL UNHANDLED EXCEPTION: {e.ExceptionObject}"); - } - - 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 - }); - } - - #endregion - } -} diff --git a/src/PowerShellEditorServices/Hosting/ProfilePaths.cs b/src/PowerShellEditorServices/Hosting/ProfilePaths.cs deleted file mode 100644 index 53eb714f1..000000000 --- a/src/PowerShellEditorServices/Hosting/ProfilePaths.cs +++ /dev/null @@ -1,108 +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.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - /// - /// Provides profile path resolution behavior relative to the name - /// of a particular PowerShell host. - /// - public class ProfilePaths - { - #region Constants - - /// - /// The file name for the "all hosts" profile. Also used as the - /// suffix for the host-specific profile filenames. - /// - public const string AllHostsProfileName = "profile.ps1"; - - #endregion - - #region Properties - - /// - /// Gets the profile path for all users, all hosts. - /// - public string AllUsersAllHosts { get; private set; } - - /// - /// Gets the profile path for all users, current host. - /// - public string AllUsersCurrentHost { get; private set; } - - /// - /// Gets the profile path for the current user, all hosts. - /// - public string CurrentUserAllHosts { get; private set; } - - /// - /// Gets the profile path for the current user and host. - /// - public string CurrentUserCurrentHost { get; private set; } - - #endregion - - #region Public Methods - - /// - /// Creates a new instance of the ProfilePaths class. - /// - /// - /// The identifier of the host used in the host-specific X_profile.ps1 filename. - /// - /// The base path to use for constructing AllUsers profile paths. - /// The base path to use for constructing CurrentUser profile paths. - public ProfilePaths( - string hostProfileId, - string baseAllUsersPath, - string baseCurrentUserPath) - { - this.Initialize(hostProfileId, baseAllUsersPath, baseCurrentUserPath); - } - - private void Initialize( - string hostProfileId, - string baseAllUsersPath, - string baseCurrentUserPath) - { - string currentHostProfileName = - string.Format( - "{0}_{1}", - hostProfileId, - AllHostsProfileName); - - this.AllUsersCurrentHost = Path.Combine(baseAllUsersPath, currentHostProfileName); - this.CurrentUserCurrentHost = Path.Combine(baseCurrentUserPath, currentHostProfileName); - this.AllUsersAllHosts = Path.Combine(baseAllUsersPath, AllHostsProfileName); - this.CurrentUserAllHosts = Path.Combine(baseCurrentUserPath, AllHostsProfileName); - } - - /// - /// Gets the list of profile paths that exist on the filesystem. - /// - /// An IEnumerable of profile path strings to be loaded. - public IEnumerable GetLoadableProfilePaths() - { - var profilePaths = - new string[] - { - this.AllUsersAllHosts, - this.AllUsersCurrentHost, - this.CurrentUserAllHosts, - this.CurrentUserCurrentHost - }; - - return profilePaths.Where(p => File.Exists(p)); - } - - #endregion - } -} - diff --git a/src/PowerShellEditorServices/Hosting/PsesLogLevel.cs b/src/PowerShellEditorServices/Hosting/PsesLogLevel.cs deleted file mode 100644 index d1b85077d..000000000 --- a/src/PowerShellEditorServices/Hosting/PsesLogLevel.cs +++ /dev/null @@ -1,45 +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 Microsoft.Extensions.Logging; - -namespace Microsoft.PowerShell.EditorServices.Hosting -{ - public enum PsesLogLevel - { - Diagnostic, - Verbose, - Normal, - Warning, - Error, - } - - internal static class PsesLogLevelExtensions - { - public static LogLevel ToExtensionsLogLevel(this PsesLogLevel logLevel) - { - switch (logLevel) - { - case PsesLogLevel.Diagnostic: - return LogLevel.Trace; - - case PsesLogLevel.Verbose: - return LogLevel.Debug; - - case PsesLogLevel.Normal: - return LogLevel.Information; - - case PsesLogLevel.Warning: - return LogLevel.Warning; - - case PsesLogLevel.Error: - return LogLevel.Error; - - default: - return LogLevel.Information; - } - } - } -} diff --git a/src/PowerShellEditorServices/Server/NamedPipePsesLanguageServer.cs b/src/PowerShellEditorServices/Server/NamedPipePsesLanguageServer.cs deleted file mode 100644 index 16f9bc749..000000000 --- a/src/PowerShellEditorServices/Server/NamedPipePsesLanguageServer.cs +++ /dev/null @@ -1,155 +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.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; -using Microsoft.PowerShell.EditorServices.Hosting; -using Microsoft.PowerShell.EditorServices.Utility; - -namespace Microsoft.PowerShell.EditorServices.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, - bool useLegacyReadLine, - HashSet featureFlags, - HostDetails hostDetails, - string[] additionalModules, - PSHost internalHost, - ProfilePaths profilePaths, - string namedPipeName, - string outNamedPipeName) : base( - factory, - minimumLogLevel, - enableConsoleRepl, - useLegacyReadLine, - 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/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 98474b0be..0bf8fc208 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -4,15 +4,19 @@ // using System; +using System.Collections.Generic; using System.IO; +using System.Management.Automation.Host; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Handlers; +using Microsoft.PowerShell.EditorServices.Hosting; using Microsoft.PowerShell.EditorServices.Services; using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Server; +using Serilog; namespace Microsoft.PowerShell.EditorServices.Server { @@ -21,6 +25,7 @@ public class PsesDebugServer : IDisposable protected readonly ILoggerFactory _loggerFactory; private readonly Stream _inputStream; private readonly Stream _outputStream; + private readonly bool _useTempSession; private IJsonRpcServer _jsonRpcServer; @@ -28,29 +33,68 @@ public class PsesDebugServer : IDisposable private readonly TaskCompletionSource _serverStopped; - public PsesDebugServer( + public static PsesDebugServer CreateWithLanguageServerServices( + ILoggerFactory loggerFactory, + Stream inputStream, + Stream outputStream, + IServiceProvider languageServerServiceProvider) + { + return new PsesDebugServer(loggerFactory, inputStream, outputStream, languageServerServiceProvider, useTempSession: false); + } + + public static PsesDebugServer CreateForTempSession( + ILoggerFactory loggerFactory, + LogLevel minimumLogLevel, + Stream inputStream, + Stream outputStream, + IReadOnlyCollection featureFlags, + HostDetails hostDetails, + IReadOnlyList additionalModules) + { + var serviceProvider = new ServiceCollection() + .AddLogging(builder => builder + .ClearProviders() + .AddSerilog() + .SetMinimumLevel(LogLevel.Trace)) + .AddSingleton(provider => null) + .AddPsesLanguageServices( + new HashSet(featureFlags, StringComparer.OrdinalIgnoreCase), + hostDetails, + additionalModules) + .BuildServiceProvider(); + + return new PsesDebugServer(loggerFactory, inputStream, outputStream, serviceProvider, useTempSession: true); + } + + private PsesDebugServer( ILoggerFactory factory, Stream inputStream, - Stream outputStream) + Stream outputStream, + IServiceProvider serviceProvider, + bool useTempSession) { _loggerFactory = factory; _inputStream = inputStream; _outputStream = outputStream; + ServiceProvider = serviceProvider; + _useTempSession = useTempSession; _serverStopped = new TaskCompletionSource(); } - public async Task StartAsync(IServiceProvider languageServerServiceProvider, bool useTempSession) + internal IServiceProvider ServiceProvider { get; } + + public async Task StartAsync() { _jsonRpcServer = await JsonRpcServer.From(options => { options.Serializer = new DapProtocolSerializer(); options.Reciever = new DapReciever(); options.LoggerFactory = _loggerFactory; - ILogger logger = options.LoggerFactory.CreateLogger("DebugOptionsStartup"); + Extensions.Logging.ILogger logger = options.LoggerFactory.CreateLogger("DebugOptionsStartup"); // We need to let the PowerShell Context Service know that we are in a debug session // so that it doesn't send the powerShell/startDebugger message. - _powerShellContextService = languageServerServiceProvider.GetService(); + _powerShellContextService = ServiceProvider.GetService(); _powerShellContextService.IsDebugServerActive = true; // Needed to make sure PSReadLine's static properties are initialized in the pipeline thread. @@ -59,7 +103,7 @@ public async Task StartAsync(IServiceProvider languageServerServiceProvider, boo .Wait(); options.Services = new ServiceCollection() - .AddPsesDebugServices(languageServerServiceProvider, this, useTempSession); + .AddPsesDebugServices(ServiceProvider, this, _useTempSession); options .WithInput(_inputStream) diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 3daf3cd52..0aa157ae7 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -3,12 +3,10 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; 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; @@ -20,41 +18,36 @@ namespace Microsoft.PowerShell.EditorServices.Server { - internal abstract class PsesLanguageServer + internal class PsesLanguageServer { internal ILoggerFactory LoggerFactory { get; private set; } + internal ILanguageServer LanguageServer { get; private set; } private readonly LogLevel _minimumLogLevel; - private readonly bool _enableConsoleRepl; - private readonly bool _useLegacyReadLine; + private readonly Stream _inputStream; + private readonly Stream _outputStream; private readonly HashSet _featureFlags; + private readonly IReadOnlyList _additionalModules; private readonly HostDetails _hostDetails; - private readonly string[] _additionalModules; - private readonly PSHost _internalHost; - private readonly ProfilePaths _profilePaths; private readonly TaskCompletionSource _serverStart; internal PsesLanguageServer( ILoggerFactory factory, LogLevel minimumLogLevel, - bool enableConsoleRepl, - bool useLegacyReadLine, - HashSet featureFlags, + Stream inputStream, + Stream outputStream, + IReadOnlyCollection featureFlags, HostDetails hostDetails, - string[] additionalModules, - PSHost internalHost, - ProfilePaths profilePaths) + IReadOnlyList additionalModules) { LoggerFactory = factory; _minimumLogLevel = minimumLogLevel; - _enableConsoleRepl = enableConsoleRepl; - _useLegacyReadLine = useLegacyReadLine; - _featureFlags = featureFlags; + _inputStream = inputStream; + _outputStream = outputStream; + _featureFlags = new HashSet(featureFlags, StringComparer.OrdinalIgnoreCase); _hostDetails = hostDetails; _additionalModules = additionalModules; - _internalHost = internalHost; - _profilePaths = profilePaths; _serverStart = new TaskCompletionSource(); } @@ -62,18 +55,12 @@ public async Task StartAsync() { LanguageServer = await OmniSharp.Extensions.LanguageServer.Server.LanguageServer.From(options => { - (Stream input, Stream output) = GetInputOutputStreams(); - options - .WithInput(input) - .WithOutput(output) + .WithInput(_inputStream) + .WithOutput(_outputStream) .WithServices(serviceCollection => serviceCollection .AddPsesLanguageServices( - _profilePaths, _featureFlags, - _enableConsoleRepl, - _useLegacyReadLine, - _internalHost, _hostDetails, _additionalModules)) .ConfigureLogging(builder => builder @@ -133,7 +120,5 @@ public async Task WaitForShutdown() await _serverStart.Task; await LanguageServer.WaitForExit; } - - protected abstract (Stream input, Stream output) GetInputOutputStreams(); } } diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs index 074178567..d0c7a2c06 100644 --- a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -17,13 +17,9 @@ internal static class PsesServiceCollectionExtensions { public static IServiceCollection AddPsesLanguageServices ( this IServiceCollection collection, - ProfilePaths profilePaths, HashSet featureFlags, - bool enableConsoleRepl, - bool useLegacyReadLine, - PSHost internalHost, HostDetails hostDetails, - string[] additionalModules) + IReadOnlyList additionalModules) { return collection.AddSingleton() .AddSingleton() @@ -33,11 +29,7 @@ public static IServiceCollection AddPsesLanguageServices ( PowerShellContextService.Create( provider.GetService(), provider.GetService(), - profilePaths, featureFlags, - enableConsoleRepl, - useLegacyReadLine, - internalHost, hostDetails, additionalModules)) .AddSingleton() diff --git a/src/PowerShellEditorServices/Server/StdioPsesLanguageServer.cs b/src/PowerShellEditorServices/Server/StdioPsesLanguageServer.cs deleted file mode 100644 index 741404cac..000000000 --- a/src/PowerShellEditorServices/Server/StdioPsesLanguageServer.cs +++ /dev/null @@ -1,44 +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.Collections.Generic; -using System.IO; -using System.Management.Automation.Host; -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Hosting; - -namespace Microsoft.PowerShell.EditorServices.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 for - // enableConsoleRepl and useLegacyReadLine. - enableConsoleRepl: false, - useLegacyReadLine: false, - featureFlags, - hostDetails, - additionalModules, - internalHost, - profilePaths) - { - - } - - protected override (Stream input, Stream output) GetInputOutputStreams() - { - return (System.Console.OpenStandardInput(), System.Console.OpenStandardOutput()); - } - } -} diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index 615c81357..34abea88b 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -168,14 +168,9 @@ public PowerShellContextService( public static PowerShellContextService Create( ILoggerFactory factory, OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, - ProfilePaths profilePaths, HashSet featureFlags, - bool enableConsoleRepl, - bool useLegacyReadLine, - PSHost internalHost, HostDetails hostDetails, - string[] additionalModules - ) + IReadOnlyList additionalModules) { var logger = factory.CreateLogger(); @@ -184,7 +179,8 @@ string[] additionalModules // We also want it if we are either: // * On Windows on any version OR // * On Linux or macOS on any version greater than or equal to 7 - bool shouldUsePSReadLine = enableConsoleRepl && !useLegacyReadLine + bool shouldUsePSReadLine = hostDetails.ConsoleReplEnabled + && !hostDetails.UsesLegacyReadLine && (VersionUtils.IsWindows || !VersionUtils.IsPS6); var powerShellContext = new PowerShellContextService( @@ -193,8 +189,8 @@ string[] additionalModules shouldUsePSReadLine); EditorServicesPSHostUserInterface hostUserInterface = - enableConsoleRepl - ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, internalHost) + hostDetails.ConsoleReplEnabled + ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, hostDetails.PSHost) : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); EditorServicesPSHost psHost = @@ -205,6 +201,7 @@ string[] additionalModules logger); Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); + var profilePaths = new ProfilePaths(hostDetails.ProfileId, hostDetails.AllUsersProfilePath, hostDetails.CurrentUserProfilePath); powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); powerShellContext.ImportCommandsModuleAsync( diff --git a/src/PowerShellEditorServices.Hosting/ProfilePaths.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ProfilePaths.cs similarity index 98% rename from src/PowerShellEditorServices.Hosting/ProfilePaths.cs rename to src/PowerShellEditorServices/Services/PowerShellContext/Session/ProfilePaths.cs index 53eb714f1..2c16be311 100644 --- a/src/PowerShellEditorServices.Hosting/ProfilePaths.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ProfilePaths.cs @@ -7,7 +7,7 @@ using System.IO; using System.Linq; -namespace Microsoft.PowerShell.EditorServices.Hosting +namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext { /// /// Provides profile path resolution behavior relative to the name diff --git a/src/PowerShellEditorServices/Hosting/HostDetails.cs b/src/PowerShellEditorServices/hosting.cs similarity index 65% rename from src/PowerShellEditorServices/Hosting/HostDetails.cs rename to src/PowerShellEditorServices/hosting.cs index cdbe96f94..53245cc65 100644 --- a/src/PowerShellEditorServices/Hosting/HostDetails.cs +++ b/src/PowerShellEditorServices/hosting.cs @@ -1,12 +1,19 @@ -// -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. See LICENSE file in the project root for full license information. -// +using System; +using System.Management.Automation.Host; +using System.Runtime.CompilerServices; -using System; +[assembly: InternalsVisibleTo("Microsoft.PowerShell.EditorServices.Hosting")] namespace Microsoft.PowerShell.EditorServices.Hosting { + internal static class EditorServicesLoading + { + internal static void LoadEditorServicesForHost() + { + // No op that forces loading this assembly + } + } + /// /// Contains details about the current host application (most /// likely the editor which is using the host process). @@ -46,18 +53,28 @@ public class HostDetails /// /// Gets the name of the host. /// - public string Name { get; private set; } + public string Name { get; } /// /// Gets the profile ID of the host, used to determine the /// host-specific profile path. /// - public string ProfileId { get; private set; } + public string ProfileId { get; } /// /// Gets the version of the host. /// - public Version Version { get; private set; } + public Version Version { get; } + + public string CurrentUserProfilePath { get; } + + public string AllUsersProfilePath { get; } + + public bool ConsoleReplEnabled { get; } + + public bool UsesLegacyReadLine { get; } + + public PSHost PSHost { get; } #endregion @@ -80,11 +97,21 @@ public class HostDetails public HostDetails( string name, string profileId, - Version version) + Version version, + PSHost psHost, + string allUsersProfilePath, + string currentUsersProfilePath, + bool consoleReplEnabled, + bool usesLegacyReadLine) { - this.Name = name ?? DefaultHostName; - this.ProfileId = profileId ?? DefaultHostProfileId; - this.Version = version ?? DefaultHostVersion; + Name = name ?? DefaultHostName; + ProfileId = profileId ?? DefaultHostProfileId; + Version = version ?? DefaultHostVersion; + PSHost = psHost; + AllUsersProfilePath = allUsersProfilePath; + CurrentUserProfilePath = currentUsersProfilePath; + ConsoleReplEnabled = consoleReplEnabled; + UsesLegacyReadLine = usesLegacyReadLine; } #endregion From ce7f50ff9221844b0ae31774d22f687c831fa246 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Thu, 21 Nov 2019 12:07:40 -0800 Subject: [PATCH 03/62] Enable compilation --- .../EditorServicesLoader.cs | 8 +------ .../EditorServicesRunner.cs | 5 ---- .../NamedPipeUtils.cs | 2 ++ .../PowerShellEditorServices.Hosting.csproj | 23 +++++++++---------- .../TransportConfig.cs | 5 ---- .../PowerShellEditorServices.csproj | 17 +++++++------- .../Server/PsesDebugServer.cs | 3 +-- .../Server/PsesLanguageServer.cs | 3 +-- src/PowerShellEditorServices/hosting.cs | 4 +--- 9 files changed, 25 insertions(+), 45 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 77686ade8..3920f0893 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -1,11 +1,5 @@ -using Microsoft.PowerShell.EditorServices; -using Microsoft.PowerShell.EditorServices.Hosting; -using Microsoft.PowerShell.EditorServices.Server; -using Serilog; +using Microsoft.PowerShell.EditorServices.Hosting; using System; -using System.Collections.ObjectModel; -using System.IO; -using System.Management.Automation; using System.Threading.Tasks; namespace PowerShellEditorServices.Hosting diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index d440b4a79..17b542cae 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -1,16 +1,11 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Hosting; using Microsoft.PowerShell.EditorServices.Server; -using Microsoft.PowerShell.EditorServices.Services.PowerShellContext; -using Microsoft.Win32; using Serilog; using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; -using System.Linq; using System.Management.Automation; -using System.Text; using System.Threading.Tasks; namespace PowerShellEditorServices.Hosting diff --git a/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs b/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs index fcf82da0c..02c71618c 100644 --- a/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs +++ b/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs @@ -11,7 +11,9 @@ namespace Microsoft.PowerShell.EditorServices.Hosting { internal static class NamedPipeUtils { +#if !CoreCLR private const int PipeBufferSize = 1024; +#endif internal static NamedPipeServerStream CreateNamedPipe( string pipeName, diff --git a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj index da3a6d099..a38f056e4 100644 --- a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj +++ b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj @@ -10,28 +10,27 @@ - + ..\..\..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\Microsoft.NET.Build.Extensions\net461\lib\System.Runtime.InteropServices.RuntimeInformation.dll + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + - + - + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - diff --git a/src/PowerShellEditorServices.Hosting/TransportConfig.cs b/src/PowerShellEditorServices.Hosting/TransportConfig.cs index 8e6c7e6cb..a98742bc2 100644 --- a/src/PowerShellEditorServices.Hosting/TransportConfig.cs +++ b/src/PowerShellEditorServices.Hosting/TransportConfig.cs @@ -1,12 +1,7 @@ using System; using System.IO; using System.IO.Pipes; - -#if !CoreCLR -using System.Security.AccessControl; -using System.Security.Principal; using System.Threading.Tasks; -#endif namespace Microsoft.PowerShell.EditorServices.Hosting { diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 022814a8b..2868ff837 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -8,25 +8,24 @@ netstandard2.0 Microsoft.PowerShell.EditorServices Latest - Debug;Release;CoreCLR + Debug;Release latest - - - - latest - - - - latest + + + + <_Parameter1>Microsoft.PowerShell.EditorServices.Hosting + + + diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 0bf8fc208..5e4edb54a 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Management.Automation.Host; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -20,7 +19,7 @@ namespace Microsoft.PowerShell.EditorServices.Server { - public class PsesDebugServer : IDisposable + internal class PsesDebugServer : IDisposable { protected readonly ILoggerFactory _loggerFactory; private readonly Stream _inputStream; diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 0aa157ae7..d2f07c66d 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Management.Automation.Host; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -32,7 +31,7 @@ internal class PsesLanguageServer private readonly HostDetails _hostDetails; private readonly TaskCompletionSource _serverStart; - internal PsesLanguageServer( + public PsesLanguageServer( ILoggerFactory factory, LogLevel minimumLogLevel, Stream inputStream, diff --git a/src/PowerShellEditorServices/hosting.cs b/src/PowerShellEditorServices/hosting.cs index 53245cc65..89954ea8a 100644 --- a/src/PowerShellEditorServices/hosting.cs +++ b/src/PowerShellEditorServices/hosting.cs @@ -2,8 +2,6 @@ using System.Management.Automation.Host; using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("Microsoft.PowerShell.EditorServices.Hosting")] - namespace Microsoft.PowerShell.EditorServices.Hosting { internal static class EditorServicesLoading @@ -44,7 +42,7 @@ public class HostDetails /// /// The default host details in a HostDetails object. /// - public static readonly HostDetails Default = new HostDetails(null, null, null); + public static readonly HostDetails Default = new HostDetails(null, null, null, null, null, null, false, false); #endregion From 9814824cec0df0526c339e8e080bbc5950efe1be Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Thu, 21 Nov 2019 17:53:29 -0800 Subject: [PATCH 04/62] Build increments --- PowerShellEditorServices.build.ps1 | 183 +++++------------- PowerShellEditorServices.sln | 12 +- .../PowerShellEditorServices.Hosting.csproj | 13 +- 3 files changed, 66 insertions(+), 142 deletions(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index aeee99828..9a704fe9a 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -19,151 +19,72 @@ param( #Requires -Modules @{ModuleName="InvokeBuild";ModuleVersion="3.2.1"} $script:IsUnix = $PSVersionTable.PSEdition -and $PSVersionTable.PSEdition -eq "Core" -and !$IsWindows -$script:TargetPlatform = "netstandard2.0" -$script:TargetFrameworksParam = "/p:TargetFrameworks=`"$script:TargetPlatform`"" $script:RequiredSdkVersion = (Get-Content (Join-Path $PSScriptRoot 'global.json') | ConvertFrom-Json).sdk.version -$script:NugetApiUriBase = 'https://www.nuget.org/api/v2/package' -$script:ModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices/bin/" -$script:VSCodeModuleBinPath = "$PSScriptRoot/module/PowerShellEditorServices.VSCode/bin/" -$script:WindowsPowerShellFrameworkTarget = 'net461' -$script:NetFrameworkPlatformId = 'win' $script:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices", "Hosting", "BuildInfo.cs") -$script:PSCoreModulePath = $null +$script:CoreRuntime = 'netcoreapp2.1' +$script:DeskRuntime = 'net461' +$script:NetStdRuntime = 'netstandard2.0' -$script:TestRuntime = @{ - 'Core' = 'netcoreapp2.1' - 'Desktop' = 'net461' -} - -<# -Declarative specification of binary assets produced -in the build that need to be binplaced in the module. -Schema is: -{ - : { - : [ - - ] - } -} -#> -$script:RequiredBuildAssets = @{ - $script:ModuleBinPath = @{ - 'PowerShellEditorServices' = @( - 'publish/Microsoft.Extensions.DependencyInjection.Abstractions.dll', - 'publish/Microsoft.Extensions.DependencyInjection.dll', - 'publish/Microsoft.Extensions.FileSystemGlobbing.dll', - 'publish/Microsoft.Extensions.Logging.Abstractions.dll', - 'publish/Microsoft.Extensions.Logging.dll', - 'publish/Microsoft.Extensions.Options.dll', - 'publish/Microsoft.Extensions.Primitives.dll', - 'publish/Microsoft.PowerShell.EditorServices.dll', - 'publish/Microsoft.PowerShell.EditorServices.pdb', - 'publish/Newtonsoft.Json.dll', - 'publish/OmniSharp.Extensions.JsonRpc.dll', - 'publish/OmniSharp.Extensions.LanguageProtocol.dll', - 'publish/OmniSharp.Extensions.LanguageServer.dll', - 'publish/OmniSharp.Extensions.DebugAdapter.dll', - 'publish/OmniSharp.Extensions.DebugAdapter.Server.dll', - 'publish/MediatR.dll', - 'publish/MediatR.Extensions.Microsoft.DependencyInjection.dll', - 'publish/runtimes/linux-64/native/libdisablekeyecho.so', - 'publish/runtimes/osx-64/native/libdisablekeyecho.dylib', - 'publish/Serilog.dll', - 'publish/Serilog.Extensions.Logging.dll', - 'publish/Serilog.Sinks.File.dll', - 'publish/System.Reactive.dll', - 'publish/UnixConsoleEcho.dll' - ) - } - - $script:VSCodeModuleBinPath = @{ - 'PowerShellEditorServices.VSCode' = @( - 'Microsoft.PowerShell.EditorServices.VSCode.dll', - 'Microsoft.PowerShell.EditorServices.VSCode.pdb' - ) - } -} +$script:HostOutput = "PowerShellEditorServices.Hosting/bin/$Configuration/" +$script:PsesOutput = "PowerShellEditorServices/bin/$Configuration/" +$script:VSCodeOutput = "PowerShellEditorServices.VSCode/bin/$Configuration/" -<# -Declares the binary shims we need to make the netstandard DLLs hook into .NET Framework. -Schema is: -{ - : [{ - 'PackageName': , - 'PackageVersion': , - 'TargetRuntime': , - 'DllName'?: - }] -} -#> -$script:RequiredNugetBinaries = @{ +$script:PsesModuleLayout = @{ + 'PowerShellEditorServices.psd1' = "$script:ModuleAssetDir/PowerShellEditorServices.psd1" + 'Core' = @( + "$script:HostOutput/$script:CoreRuntime/Microsoft.PowerShell.EditorServices.Hosting.dll" + ) 'Desktop' = @( - @{ PackageName = 'System.Security.Principal.Windows'; PackageVersion = '4.5.0'; TargetRuntime = 'net461' }, - @{ PackageName = 'System.Security.AccessControl'; PackageVersion = '4.5.0'; TargetRuntime = 'net461' }, - @{ PackageName = 'System.IO.Pipes.AccessControl'; PackageVersion = '4.5.1'; TargetRuntime = 'net461' } + "$script:HostOutput/$script:DesktopRuntime/Microsoft.PowerShell.EditorServices.Hosting.dll" + "$script:HostOutput/$script:DesktopRuntime/System.IO.Pipes.AccessControl.dll" + "$script:HostOutput/$script:DesktopRuntime/System.Security.AccessControl.dll" + "$script:HostOutput/$script:DesktopRuntime/System.Security.Principal.Windows.dll" + ) + 'Dependencies' = @( + "$script:PsesOutput/$script:NetStdRuntime/Microsoft.PowerShell.EditorServices.dll" + "$script:PsesOutput/$script:NetStdRuntime/Microsoft.PowerShell.EditorServices.pdb", + + "$script:PsesOutput/$script:NetStdRuntime/runtimes/linux-64/native/libdisablekeyecho.so", + "$script:PsesOutput/$script:NetStdRuntime/runtimes/osx-64/native/libdisablekeyecho.dylib", + "$script:PsesOutput/$script:NetStdRuntime/UnixConsoleEcho.dll" + + "$script:PsesOutput/$script:NetStdRuntime/OmniSharp.Extensions.JsonRpc.dll", + "$script:PsesOutput/$script:NetStdRuntime/OmniSharp.Extensions.LanguageProtocol.dll", + "$script:PsesOutput/$script:NetStdRuntime/OmniSharp.Extensions.LanguageServer.dll", + "$script:PsesOutput/$script:NetStdRuntime/OmniSharp.Extensions.DebugAdapter.dll", + "$script:PsesOutput/$script:NetStdRuntime/OmniSharp.Extensions.DebugAdapter.Server.dll", + + "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.DependencyInjection.Abstractions.dll", + "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.DependencyInjection.dll", + "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.FileSystemGlobbing.dll", + "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.Logging.Abstractions.dll", + "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.Logging.dll", + "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.Options.dll", + "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.Primitives.dll", + "$script:PsesOutput/$script:NetStdRuntime/MediatR.dll", + "$script:PsesOutput/$script:NetStdRuntime/MediatR.Extensions.Microsoft.DependencyInjection.dll", + + "$script:PsesOutput/$script:NetStdRuntime/Newtonsoft.Json.dll", + + "$script:PsesOutput/$script:NetStdRuntime/Serilog.dll", + "$script:PsesOutput/$script:NetStdRuntime/Serilog.Extensions.Logging.dll", + "$script:PsesOutput/$script:NetStdRuntime/Serilog.Sinks.File.dll", + + "$script:PsesOutput/$script:NetStdRuntime/System.Reactive.dll" ) } +$script:PsesVSCodeModuleLayout = @{ + 'Microsoft.PowerShell.EditorServices.VSCode.dll' = "$script:VSCodeOutput/$script:NetStdRuntime/Microsoft.PowerShell.EditorServices.VSCode.dll" + 'Microsoft.PowerShell.EditorServices.VSCode.pdb' = "$script:VSCodeOutput/$script:NetStdRuntime/Microsoft.PowerShell.EditorServices.VSCode.pdb" +} + if (Get-Command git -ErrorAction SilentlyContinue) { # ignore changes to this file git update-index --assume-unchanged "$PSScriptRoot/src/PowerShellEditorServices.Host/BuildInfo/BuildInfo.cs" } -if ($PSVersionTable.PSEdition -ne "Core") { - Add-Type -Assembly System.IO.Compression.FileSystem -} - -function Restore-NugetAsmForRuntime { - param( - [ValidateNotNull()][string]$PackageName, - [ValidateNotNull()][string]$PackageVersion, - [string]$DllName, - [string]$DestinationPath, - [string]$TargetPlatform = $script:NetFrameworkPlatformId, - [string]$TargetRuntime = $script:WindowsPowerShellFrameworkTarget - ) - - $tmpDir = Join-Path $PSScriptRoot '.tmp' - if (-not (Test-Path $tmpDir)) { - New-Item -ItemType Directory -Path $tmpDir - } - - if (-not $DllName) { - $DllName = "$PackageName.dll" - } - - if ($DestinationPath -eq $null) { - $DestinationPath = Join-Path $tmpDir $DllName - } elseif (Test-Path $DestinationPath -PathType Container) { - $DestinationPath = Join-Path $DestinationPath $DllName - } - - $packageDirPath = Join-Path $tmpDir "$PackageName.$PackageVersion" - if (-not (Test-Path $packageDirPath)) { - $guid = New-Guid - $tmpNupkgPath = Join-Path $tmpDir "$guid.zip" - if (Test-Path $tmpNupkgPath) { - Remove-Item -Force $tmpNupkgPath - } - - try { - $packageUri = "$script:NugetApiUriBase/$PackageName/$PackageVersion" - Invoke-WebRequest -Uri $packageUri -OutFile $tmpNupkgPath - Expand-Archive -Path $tmpNupkgPath -DestinationPath $packageDirPath - } finally { - Remove-Item -Force $tmpNupkgPath -ErrorAction SilentlyContinue - } - } - - $internalPath = [System.IO.Path]::Combine($packageDirPath, 'runtimes', $TargetPlatform, 'lib', $TargetRuntime, $DllName) - - Copy-Item -Path $internalPath -Destination $DestinationPath -Force - - return $DestinationPath -} - function Invoke-WithCreateDefaultHook { param([scriptblock]$ScriptBlock) diff --git a/PowerShellEditorServices.sln b/PowerShellEditorServices.sln index aab69e80e..802579fef 100644 --- a/PowerShellEditorServices.sln +++ b/PowerShellEditorServices.sln @@ -131,12 +131,12 @@ Global {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x64.Build.0 = Release|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x86.ActiveCfg = Release|Any CPU {3B38E8DA-8BFF-4264-AF16-47929E6398A3}.Release|x86.Build.0 = Release|Any CPU - {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|Any CPU.ActiveCfg = CoreCLR|Any CPU - {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|Any CPU.Build.0 = CoreCLR|Any CPU - {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x64.ActiveCfg = CoreCLR|Any CPU - {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x64.Build.0 = CoreCLR|Any CPU - {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x86.ActiveCfg = CoreCLR|Any CPU - {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x86.Build.0 = CoreCLR|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|Any CPU.ActiveCfg = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|Any CPU.Build.0 = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x64.ActiveCfg = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x64.Build.0 = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x86.ActiveCfg = Release|Any CPU + {29EEDF03-0990-45F4-846E-2616970D1FA2}.CoreCLR|x86.Build.0 = Release|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|Any CPU.Build.0 = Debug|Any CPU {29EEDF03-0990-45F4-846E-2616970D1FA2}.Debug|x64.ActiveCfg = Debug|Any CPU diff --git a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj index a38f056e4..ca05c356b 100644 --- a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj +++ b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj @@ -10,26 +10,29 @@ - + + ..\..\..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\Microsoft.NET.Build.Extensions\net461\lib\System.Runtime.InteropServices.RuntimeInformation.dll + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + - + From a2ab488a18168a4dbcb7dd34157560135d7a7c06 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Fri, 22 Nov 2019 18:42:50 -0800 Subject: [PATCH 05/62] Add initial load context --- PowerShellEditorServices.build.ps1 | 152 +++----- .../PowerShellEditorServices.psd1 | 13 +- .../Start-EditorServices.ps1 | 340 ------------------ .../EditorServicesLoader.cs | 54 ++- .../EditorServicesRunner.cs | 107 ++---- .../HostLogger.cs | 10 +- .../PowerShellEditorServices.Hosting.csproj | 5 +- .../PsesLoadContext.cs | 31 ++ .../StartEditorServicesCommand.cs | 71 ++-- .../Hosting/EditorServicesLoading.cs | 11 + .../Hosting/EditorServicesServerFactory.cs | 60 ++++ .../HostStartupInfo.cs} | 37 +- .../PowerShellEditorServices.csproj | 2 + .../Server/PsesDebugServer.cs | 9 +- .../Server/PsesLanguageServer.cs | 15 +- .../Server/PsesServiceCollectionExtensions.cs | 10 +- .../PowerShellContextService.cs | 8 +- .../Session/Host/EditorServicesPSHost.cs | 4 +- 18 files changed, 347 insertions(+), 592 deletions(-) create mode 100644 src/PowerShellEditorServices.Hosting/PsesLoadContext.cs create mode 100644 src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs create mode 100644 src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs rename src/PowerShellEditorServices/{hosting.cs => Hosting/HostStartupInfo.cs} (83%) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 9a704fe9a..a9718ffe4 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -22,63 +22,16 @@ $script:IsUnix = $PSVersionTable.PSEdition -and $PSVersionTable.PSEdition -eq "C $script:RequiredSdkVersion = (Get-Content (Join-Path $PSScriptRoot 'global.json') | ConvertFrom-Json).sdk.version $script:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices", "Hosting", "BuildInfo.cs") -$script:CoreRuntime = 'netcoreapp2.1' -$script:DeskRuntime = 'net461' -$script:NetStdRuntime = 'netstandard2.0' - -$script:HostOutput = "PowerShellEditorServices.Hosting/bin/$Configuration/" -$script:PsesOutput = "PowerShellEditorServices/bin/$Configuration/" -$script:VSCodeOutput = "PowerShellEditorServices.VSCode/bin/$Configuration/" - -$script:PsesModuleLayout = @{ - 'PowerShellEditorServices.psd1' = "$script:ModuleAssetDir/PowerShellEditorServices.psd1" - 'Core' = @( - "$script:HostOutput/$script:CoreRuntime/Microsoft.PowerShell.EditorServices.Hosting.dll" - ) - 'Desktop' = @( - "$script:HostOutput/$script:DesktopRuntime/Microsoft.PowerShell.EditorServices.Hosting.dll" - "$script:HostOutput/$script:DesktopRuntime/System.IO.Pipes.AccessControl.dll" - "$script:HostOutput/$script:DesktopRuntime/System.Security.AccessControl.dll" - "$script:HostOutput/$script:DesktopRuntime/System.Security.Principal.Windows.dll" - ) - 'Dependencies' = @( - "$script:PsesOutput/$script:NetStdRuntime/Microsoft.PowerShell.EditorServices.dll" - "$script:PsesOutput/$script:NetStdRuntime/Microsoft.PowerShell.EditorServices.pdb", - - "$script:PsesOutput/$script:NetStdRuntime/runtimes/linux-64/native/libdisablekeyecho.so", - "$script:PsesOutput/$script:NetStdRuntime/runtimes/osx-64/native/libdisablekeyecho.dylib", - "$script:PsesOutput/$script:NetStdRuntime/UnixConsoleEcho.dll" - - "$script:PsesOutput/$script:NetStdRuntime/OmniSharp.Extensions.JsonRpc.dll", - "$script:PsesOutput/$script:NetStdRuntime/OmniSharp.Extensions.LanguageProtocol.dll", - "$script:PsesOutput/$script:NetStdRuntime/OmniSharp.Extensions.LanguageServer.dll", - "$script:PsesOutput/$script:NetStdRuntime/OmniSharp.Extensions.DebugAdapter.dll", - "$script:PsesOutput/$script:NetStdRuntime/OmniSharp.Extensions.DebugAdapter.Server.dll", - - "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.DependencyInjection.Abstractions.dll", - "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.DependencyInjection.dll", - "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.FileSystemGlobbing.dll", - "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.Logging.Abstractions.dll", - "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.Logging.dll", - "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.Options.dll", - "$script:PsesOutput/$script:NetStdRuntime/Microsoft.Extensions.Primitives.dll", - "$script:PsesOutput/$script:NetStdRuntime/MediatR.dll", - "$script:PsesOutput/$script:NetStdRuntime/MediatR.Extensions.Microsoft.DependencyInjection.dll", - - "$script:PsesOutput/$script:NetStdRuntime/Newtonsoft.Json.dll", - - "$script:PsesOutput/$script:NetStdRuntime/Serilog.dll", - "$script:PsesOutput/$script:NetStdRuntime/Serilog.Extensions.Logging.dll", - "$script:PsesOutput/$script:NetStdRuntime/Serilog.Sinks.File.dll", - - "$script:PsesOutput/$script:NetStdRuntime/System.Reactive.dll" - ) +$script:NetRuntime = @{ + Core = 'netcoreapp2.1' + Desktop = 'net461' + Standard = 'netstandard2.0' } -$script:PsesVSCodeModuleLayout = @{ - 'Microsoft.PowerShell.EditorServices.VSCode.dll' = "$script:VSCodeOutput/$script:NetStdRuntime/Microsoft.PowerShell.EditorServices.VSCode.dll" - 'Microsoft.PowerShell.EditorServices.VSCode.pdb' = "$script:VSCodeOutput/$script:NetStdRuntime/Microsoft.PowerShell.EditorServices.VSCode.pdb" -} +$script:HostCoreOutput = "$PSScriptRoot/src/PowerShellEditorServices.Hosting/bin/$Configuration/$($script:NetRuntime.Core)/publish" +$script:HostDeskOutput = "$PSScriptRoot/src/PowerShellEditorServices.Hosting/bin/$Configuration/$($script:NetRuntime.Desktop)/publish" +$script:PsesOutput = "$PSScriptRoot/src/PowerShellEditorServices/bin/$Configuration/$($script:NetRuntime.Standard)/publish" +$script:VSCodeOutput = "$PSScriptRoot/src/PowerShellEditorServices.VSCode/bin/$Configuration/$($script:NetRuntime.Standard)/publish" if (Get-Command git -ErrorAction SilentlyContinue) { # ignore changes to this file @@ -248,8 +201,13 @@ task SetupHelpForTests -Before Test { } task Build { - exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices\PowerShellEditorServices.csproj -f $script:TargetPlatform } - exec { & $script:dotnetExe build -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj $script:TargetFrameworksParam } + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices\PowerShellEditorServices.csproj -f $script:NetRuntime.Standard } + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Hosting\PowerShellEditorServices.Hosting.csproj -f $script:NetRuntime.Core } + if (-not $script:IsUnix) + { + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Hosting\PowerShellEditorServices.Hosting.csproj -f $script:NetRuntime.Desktop } + } + exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj -f $script:NetRuntime.Standard } } function DotNetTestFilter { @@ -264,11 +222,11 @@ task TestServer { Set-Location .\test\PowerShellEditorServices.Test\ if (-not $script:IsUnix) { - exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Desktop (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Desktop (DotNetTestFilter) } } Invoke-WithCreateDefaultHook -NewModulePath $script:PSCoreModulePath { - exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Core (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Core (DotNetTestFilter) } } } @@ -276,11 +234,11 @@ task TestProtocol { Set-Location .\test\PowerShellEditorServices.Test.Protocol\ if (-not $script:IsUnix) { - exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Desktop (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Desktop (DotNetTestFilter) } } Invoke-WithCreateDefaultHook { - exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Core (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Core (DotNetTestFilter) } } } @@ -288,56 +246,62 @@ task TestHost { Set-Location .\test\PowerShellEditorServices.Test.Host\ if (-not $script:IsUnix) { - exec { & $script:dotnetExe build -f $script:TestRuntime.Desktop } - exec { & $script:dotnetExe test -f $script:TestRuntime.Desktop (DotNetTestFilter) } + exec { & $script:dotnetExe build -f $script:NetRuntime.Desktop } + exec { & $script:dotnetExe test -f $script:NetRuntime.Desktop (DotNetTestFilter) } } - exec { & $script:dotnetExe build -c $Configuration -f $script:TestRuntime.Core } - exec { & $script:dotnetExe test -f $script:TestRuntime.Core (DotNetTestFilter) } + exec { & $script:dotnetExe build -c $Configuration -f $script:NetRuntime.Core } + exec { & $script:dotnetExe test -f $script:NetRuntime.Core (DotNetTestFilter) } } task TestE2E { Set-Location .\test\PowerShellEditorServices.Test.E2E\ $env:PWSH_EXE_NAME = if ($IsCoreCLR) { "pwsh" } else { "powershell" } - exec { & $script:dotnetExe test --logger trx -f $script:TestRuntime.Core (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Core (DotNetTestFilter) } } task LayoutModule -After Build { + $psesOutputPath = "$PSScriptRoot/module/PowerShellEditorServices" + $psesBinOutputPath = "$PSScriptRoot/module/PowerShellEditorServices/bin" + $psesDepsPath = "$psesBinOutputPath/Common" + $psesCoreHostPath = "$psesBinOutputPath/Core" + $psesDeskHostPath = "$psesBinOutputPath/Desktop" + + foreach ($dir in $psesDepsPath,$psesCoreHostPath,$psesDeskHostPath) + { + New-Item -Force -Path $dir -ItemType Directory + } + # Copy Third Party Notices.txt to module folder - Copy-Item -Force -Path "$PSScriptRoot\Third Party Notices.txt" -Destination $PSScriptRoot\module\PowerShellEditorServices - - # Lay out the PowerShellEditorServices module's binaries - # For each binplace destination - foreach ($destDir in $script:RequiredBuildAssets.Keys) { - # Create the destination dir - $null = New-Item -Force $destDir -Type Directory - - # For each PSES subproject - foreach ($projectName in $script:RequiredBuildAssets[$destDir].Keys) { - # Get the project build dir path - $basePath = [System.IO.Path]::Combine($PSScriptRoot, 'src', $projectName, 'bin', $Configuration, $script:TargetPlatform) - - # For each asset in the subproject - foreach ($bin in $script:RequiredBuildAssets[$destDir][$projectName]) { - # Get the asset path - $binPath = Join-Path $basePath $bin - - # Binplace the asset - Copy-Item -Force -Verbose $binPath $destDir - } + Copy-Item -Force -Path "$PSScriptRoot\Third Party Notices.txt" -Destination $psesOutputPath + + $psesDlls = [System.Collections.Generic.HashSet[string]]::new() + foreach ($psesComponent in Get-ChildItem $script:PsesOutput) + { + if ($psesComponent.Extension -eq '.dll') + { + [void]$psesDlls.Add($psesComponent.Name) + Copy-Item -Path $psesComponent.FullName -Destination $psesDepsPath } } - # Get and place the shim bins for net461 - foreach ($binDestinationDir in $script:RequiredNugetBinaries.Keys) { - $binDestPath = Join-Path $script:ModuleBinPath $binDestinationDir - if (-not (Test-Path $binDestPath)) { - New-Item -Path $binDestPath -ItemType Directory + foreach ($hostComponent in Get-ChildItem $script:HostCoreOutput) + { + if (-not $psesDlls.Contains($hostComponent.Name)) + { + Copy-Item -Path $hostComponent.FullName -Destination $psesCoreHostPath } + } - foreach ($packageDetails in $script:RequiredNugetBinaries[$binDestinationDir]) { - Restore-NugetAsmForRuntime -DestinationPath $binDestPath @packageDetails + if (-not $script:IsUnix) + { + foreach ($hostComponent in Get-ChildItem $script:HostDeskOutput) + { + if (-not $psesDlls.Contains($hostComponent.Name)) + { + Copy-Item -Path $hostComponent.FullName -Destination $psesDeskHostPath + } } } } diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psd1 b/module/PowerShellEditorServices/PowerShellEditorServices.psd1 index 7a03e12be..4c5887f4f 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psd1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psd1 @@ -9,7 +9,14 @@ @{ # Script module or binary module file associated with this manifest. -RootModule = 'PowerShellEditorServices.psm1' +RootModule = if ($PSEdition -eq 'Core') + { + 'bin/Core/Microsoft.PowerShell.EditorServices.Hosting.dll' + } + else + { + 'bin/Desktop/Microsoft.PowerShell.EditorServices.Hosting.dll' + } # Version number of this module. ModuleVersion = '2.0.0' @@ -66,10 +73,10 @@ Copyright = '(c) 2017 Microsoft. All rights reserved.' # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @('Start-EditorServicesHost', 'Get-PowerShellEditorServicesVersion', 'Compress-LogDir') +FunctionsToExport = @() # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = @() +CmdletsToExport = @('Start-EditorServices') # Variables to export from this module VariablesToExport = @() diff --git a/module/PowerShellEditorServices/Start-EditorServices.ps1 b/module/PowerShellEditorServices/Start-EditorServices.ps1 index 5f828a905..0b1499073 100644 --- a/module/PowerShellEditorServices/Start-EditorServices.ps1 +++ b/module/PowerShellEditorServices/Start-EditorServices.ps1 @@ -108,343 +108,3 @@ param( $DebugServiceOutPipeName = $null ) -$DEFAULT_USER_MODE = "600" - -if ($LogLevel -eq "Diagnostic") { - if (!$Stdio.IsPresent) { - $VerbosePreference = 'Continue' - } - $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name) - $logFileName = [System.IO.Path]::GetFileName($LogPath) - Start-Transcript (Join-Path (Split-Path $LogPath -Parent) "$scriptName-$logFileName") -Force | Out-Null -} - -function LogSection([string]$msg) { - Write-Verbose "`n#-- $msg $('-' * ([Math]::Max(0, 73 - $msg.Length)))" -} - -function Log([string[]]$msg) { - $msg | Write-Verbose -} - -function ExitWithError($errorString) { - Write-Host -ForegroundColor Red "`n`n$errorString" - - # Sleep for a while to make sure the user has time to see and copy the - # error message - Start-Sleep -Seconds 300 - - exit 1; -} - -function WriteSessionFile($sessionInfo) { - $sessionInfoJson = Microsoft.PowerShell.Utility\ConvertTo-Json -InputObject $sessionInfo -Compress - Log "Writing session file with contents:" - Log $sessionInfoJson - $sessionInfoJson | Microsoft.PowerShell.Management\Set-Content -Force -Path "$SessionDetailsPath" -ErrorAction Stop -} - -# Are we running in PowerShell 2 or earlier? -$version = $PSVersionTable.PSVersion -if (($version.Major -le 2) -or ($version.Major -eq 6 -and $version.Minor -eq 0)) { - # No ConvertTo-Json on PSv2 and below, so write out the JSON manually - "{`"status`": `"failed`", `"reason`": `"unsupported`", `"powerShellVersion`": `"$($PSVersionTable.PSVersion.ToString())`"}" | - Microsoft.PowerShell.Management\Set-Content -Force -Path "$SessionDetailsPath" -ErrorAction Stop - - ExitWithError "Unsupported PowerShell version $($PSVersionTable.PSVersion), language features are disabled." -} - - -if ($host.Runspace.LanguageMode -eq 'ConstrainedLanguage') { - WriteSessionFile @{ - "status" = "failed" - "reason" = "languageMode" - "detail" = $host.Runspace.LanguageMode.ToString() - } - - ExitWithError "PowerShell is configured with an unsupported LanguageMode (ConstrainedLanguage), language features are disabled." -} - -# net451 and lower are not supported, only net452 and up -if ($PSVersionTable.PSVersion.Major -le 5) { - $net452Version = 379893 - $dotnetVersion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\").Release - if ($dotnetVersion -lt $net452Version) { - Write-SessionFile @{ - status = failed - reason = "netversion" - detail = "$netVersion" - } - - ExitWithError "Your .NET version is too low. Upgrade to net452 or higher to run the PowerShell extension." - } -} - -# If PSReadline is present in the session, remove it so that runspace -# management is easier -if ((Microsoft.PowerShell.Core\Get-Module PSReadline).Count -gt 0) { - LogSection "Removing PSReadLine module" - Microsoft.PowerShell.Core\Remove-Module PSReadline -ErrorAction SilentlyContinue -} - -# This variable will be assigned later to contain information about -# what happened while attempting to launch the PowerShell Editor -# Services host -$resultDetails = $null; - -function Test-ModuleAvailable($ModuleName, $ModuleVersion) { - Log "Testing module availability $ModuleName $ModuleVersion" - - $modules = Microsoft.PowerShell.Core\Get-Module -ListAvailable $moduleName - if ($null -ne $modules) { - if ($null -ne $ModuleVersion) { - foreach ($module in $modules) { - if ($module.Version.Equals($moduleVersion)) { - Log "$ModuleName $ModuleVersion found" - return $true; - } - } - } - else { - Log "$ModuleName $ModuleVersion found" - return $true; - } - } - - Log "$ModuleName $ModuleVersion NOT found" - return $false; -} - -function New-NamedPipeName { - # We try 10 times to find a valid pipe name - for ($i = 0; $i -lt 10; $i++) { - $PipeName = "PSES_$([System.IO.Path]::GetRandomFileName())" - - if ((Test-NamedPipeName -PipeName $PipeName)) { - return $PipeName - } - } - - ExitWithError "Could not find valid a pipe name." -} - -function Get-NamedPipePath { - param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $PipeName - ) - - if (($PSVersionTable.PSVersion.Major -le 5) -or $IsWindows) { - return "\\.\pipe\$PipeName"; - } - else { - # Windows uses NamedPipes where non-Windows platforms use Unix Domain Sockets. - # the Unix Domain Sockets live in the tmp directory and are prefixed with "CoreFxPipe_" - return (Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath "CoreFxPipe_$PipeName") - } -} - -# Returns True if it's a valid pipe name -# A valid pipe name is a file that does not exist either -# in the temp directory (macOS & Linux) or in the pipe directory (Windows) -function Test-NamedPipeName { - param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $PipeName - ) - - $path = Get-NamedPipePath -PipeName $PipeName - return !(Test-Path $path) -} - -LogSection "Console Encoding" -Log $OutputEncoding - -function Get-ValidatedNamedPipeName { - param( - [string] - $PipeName - ) - - # If no PipeName is passed in, then we create one that's guaranteed to be valid - if (!$PipeName) { - $PipeName = New-NamedPipeName - } - elseif (!(Test-NamedPipeName -PipeName $PipeName)) { - ExitWithError "Pipe name supplied is already in use: $PipeName" - } - - return $PipeName -} - -function Set-PipeFileResult { - param ( - [Hashtable] - $ResultTable, - - [string] - $PipeNameKey, - - [string] - $PipeNameValue - ) - - $ResultTable[$PipeNameKey] = Get-NamedPipePath -PipeName $PipeNameValue -} - -# Add BundledModulesPath to $env:PSModulePath -if ($BundledModulesPath) { - $env:PSModulePath = $env:PSModulePath.TrimEnd([System.IO.Path]::PathSeparator) + [System.IO.Path]::PathSeparator + $BundledModulesPath - LogSection "Updated PSModulePath to:" - Log ($env:PSModulePath -split [System.IO.Path]::PathSeparator) -} - -LogSection "Check required modules available" -# Check if PowerShellGet module is available -if ((Test-ModuleAvailable "PowerShellGet") -eq $false) { - Log "Failed to find PowerShellGet module" - # TODO: WRITE ERROR -} - -try { - LogSection "Start up PowerShellEditorServices" - Log "Importing PowerShellEditorServices" - - Microsoft.PowerShell.Core\Import-Module PowerShellEditorServices -ErrorAction Stop - - if ($EnableConsoleRepl) { - Write-Host "PowerShell Integrated Console`n" - } - - $resultDetails = @{ - "status" = "not started"; - "languageServiceTransport" = $PSCmdlet.ParameterSetName; - "debugServiceTransport" = $PSCmdlet.ParameterSetName; - } - - # Create the Editor Services host - Log "Invoking Start-EditorServicesHost" - # There could be only one service on Stdio channel - # Locate available port numbers for services - switch ($PSCmdlet.ParameterSetName) { - "Stdio" { - $editorServicesHost = Start-EditorServicesHost ` - -HostName $HostName ` - -HostProfileId $HostProfileId ` - -HostVersion $HostVersion ` - -LogPath $LogPath ` - -LogLevel $LogLevel ` - -AdditionalModules $AdditionalModules ` - -Stdio ` - -BundledModulesPath $BundledModulesPath ` - -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` - -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent ` - -FeatureFlags $FeatureFlags - break - } - - "NamedPipeSimplex" { - $LanguageServiceInPipeName = Get-ValidatedNamedPipeName $LanguageServiceInPipeName - $LanguageServiceOutPipeName = Get-ValidatedNamedPipeName $LanguageServiceOutPipeName - $DebugServiceInPipeName = Get-ValidatedNamedPipeName $DebugServiceInPipeName - $DebugServiceOutPipeName = Get-ValidatedNamedPipeName $DebugServiceOutPipeName - - $editorServicesHost = Start-EditorServicesHost ` - -HostName $HostName ` - -HostProfileId $HostProfileId ` - -HostVersion $HostVersion ` - -LogPath $LogPath ` - -LogLevel $LogLevel ` - -AdditionalModules $AdditionalModules ` - -LanguageServiceInNamedPipe $LanguageServiceInPipeName ` - -LanguageServiceOutNamedPipe $LanguageServiceOutPipeName ` - -DebugServiceInNamedPipe $DebugServiceInPipeName ` - -DebugServiceOutNamedPipe $DebugServiceOutPipeName ` - -BundledModulesPath $BundledModulesPath ` - -UseLegacyReadLine:$UseLegacyReadLine.IsPresent ` - -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` - -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent ` - -FeatureFlags $FeatureFlags - - Set-PipeFileResult $resultDetails "languageServiceReadPipeName" $LanguageServiceInPipeName - Set-PipeFileResult $resultDetails "languageServiceWritePipeName" $LanguageServiceOutPipeName - Set-PipeFileResult $resultDetails "debugServiceReadPipeName" $DebugServiceInPipeName - Set-PipeFileResult $resultDetails "debugServiceWritePipeName" $DebugServiceOutPipeName - break - } - - Default { - $LanguageServicePipeName = Get-ValidatedNamedPipeName $LanguageServicePipeName - $DebugServicePipeName = Get-ValidatedNamedPipeName $DebugServicePipeName - - $editorServicesHost = Start-EditorServicesHost ` - -HostName $HostName ` - -HostProfileId $HostProfileId ` - -HostVersion $HostVersion ` - -LogPath $LogPath ` - -LogLevel $LogLevel ` - -AdditionalModules $AdditionalModules ` - -LanguageServiceNamedPipe $LanguageServicePipeName ` - -DebugServiceNamedPipe $DebugServicePipeName ` - -BundledModulesPath $BundledModulesPath ` - -UseLegacyReadLine:$UseLegacyReadLine.IsPresent ` - -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` - -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent ` - -FeatureFlags $FeatureFlags - - Set-PipeFileResult $resultDetails "languageServicePipeName" $LanguageServicePipeName - Set-PipeFileResult $resultDetails "debugServicePipeName" $DebugServicePipeName - break - } - } - - # TODO: Verify that the service is started - Log "Start-EditorServicesHost returned $editorServicesHost" - - $resultDetails["status"] = "started" - - # Notify the client that the services have started - WriteSessionFile $resultDetails - - Log "Wrote out session file" -} -catch [System.Exception] { - $e = $_.Exception; - $errorString = "" - - Log "ERRORS caught starting up EditorServicesHost" - - while ($null -ne $e) { - $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") - $e = $e.InnerException; - Log $errorString - } - - ExitWithError ("An error occurred while starting PowerShell Editor Services:`r`n`r`n" + $errorString) -} - -try { - # Wait for the host to complete execution before exiting - LogSection "Waiting for EditorServicesHost to complete execution" - $editorServicesHost.WaitForCompletion() - Log "EditorServicesHost has completed execution" -} -catch [System.Exception] { - $e = $_.Exception; - $errorString = "" - - Log "ERRORS caught while waiting for EditorServicesHost to complete execution" - - while ($null -ne $e) { - $errorString = $errorString + ($e.Message + "`r`n" + $e.StackTrace + "`r`n") - $e = $e.InnerException; - Log $errorString - } -} diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 3920f0893..5ed9f57d3 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -1,18 +1,36 @@ using Microsoft.PowerShell.EditorServices.Hosting; using System; +using System.IO; +using System.Reflection; using System.Threading.Tasks; +#if CoreCLR +using System.Runtime.Loader; +#endif + namespace PowerShellEditorServices.Hosting { public sealed class EditorServicesLoader : IDisposable { private const int Net461Version = 394254; - private static readonly string s_dependencyPath = null; + private static readonly string s_psesDependencyDirPath = Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + "..", + "Common")); + +#if CoreCLR + private static readonly AssemblyLoadContext s_coreAsmLoadContext = new PsesLoadContext(s_psesDependencyDirPath); +#endif public static EditorServicesLoader Create(EditorServicesConfig hostConfig, string dependencyPath = null) { - // TODO: Register assembly resolve event +#if CoreCLR + AssemblyLoadContext.Default.Resolving += DefaultLoadContext_OnAssemblyResolve; +#else + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_OnAssemblyResolve; +#endif return new EditorServicesLoader(hostConfig); } @@ -39,5 +57,37 @@ public void Dispose() { // TODO: Deregister assembly event } + +#if CoreCLR + private static Assembly DefaultLoadContext_OnAssemblyResolve(AssemblyLoadContext defaultLoadContext, AssemblyName asmName) + { + Console.WriteLine($".NET Core resolving {asmName}"); + + if (!string.Equals(asmName.Name, "Microsoft.PowerShell.EditorServices", StringComparison.Ordinal)) + { + return null; + } + + string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); + + return s_coreAsmLoadContext.LoadFromAssemblyPath(asmPath); + } +#endif + +#if !CoreCLR + private static Assembly CurrentDomain_OnAssemblyResolve(object sender, ResolveEventArgs args) + { + Console.WriteLine($".NET FX resolving {args.Name}"); + + var asmName = new AssemblyName(args.Name); + + string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); + + return File.Exists(asmPath) + ? Assembly.LoadFrom(asmPath) + : null; + } +#endif + } } diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 17b542cae..59bd33ca9 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -1,7 +1,5 @@ -using Microsoft.Extensions.Logging; -using Microsoft.PowerShell.EditorServices.Hosting; +using Microsoft.PowerShell.EditorServices.Hosting; using Microsoft.PowerShell.EditorServices.Server; -using Serilog; using System; using System.Collections.ObjectModel; using System.IO; @@ -14,28 +12,19 @@ internal class EditorServicesRunner : IDisposable { public static EditorServicesRunner Create(EditorServicesConfig config) { - Log.Logger = new LoggerConfiguration() - .Enrich.FromLogContext() - .WriteTo.File(config.LogPath) - .MinimumLevel.Verbose() - .CreateLogger(); - - return new EditorServicesRunner(new LoggerFactory().AddSerilog(Log.Logger), config); + return new EditorServicesRunner(config); } - private readonly ILoggerFactory _loggerFactory; - - private readonly Microsoft.Extensions.Logging.ILogger _logger; - private readonly EditorServicesConfig _config; + private readonly EditorServicesServerFactory _serverFactory; + private bool _alreadySubscribedDebug; - private EditorServicesRunner(ILoggerFactory loggerFactory, EditorServicesConfig config) + private EditorServicesRunner(EditorServicesConfig config) { - _loggerFactory = loggerFactory; - _logger = _loggerFactory.CreateLogger(); _config = config; + _serverFactory = EditorServicesServerFactory.Create(_config.LogPath, (int)_config.LogLevel); _alreadySubscribedDebug = false; } @@ -51,30 +40,32 @@ public async Task RunUntilShutdown() ProfilePathConfig profilePaths = GetProfilePaths(_config.ProfilePaths); - var hostDetails = new HostDetails( + var hostStartupInfo = new HostStartupInfo( _config.HostInfo.Name, _config.HostInfo.ProfileId, _config.HostInfo.Version, _config.PSHost, profilePaths.AllUsersProfilePath, profilePaths.CurrentUserProfilePath, + _config.FeatureFlags, + _config.AdditionalModules, + _config.LogPath, + (int)_config.LogLevel, consoleReplEnabled: _config.ConsoleRepl != ConsoleReplKind.None, usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine); - LogLevel minimumLogLevel = ConvertToExtensionLogLevel(_config.LogLevel); - if (isTempDebugSession) { - await RunTempDebugSession(hostDetails, minimumLogLevel).ConfigureAwait(false); + await RunTempDebugSession(hostStartupInfo).ConfigureAwait(false); return; } - PsesLanguageServer languageServer = await CreateLanguageServer(hostDetails, minimumLogLevel).ConfigureAwait(false); + PsesLanguageServer languageServer = await CreateLanguageServer(hostStartupInfo).ConfigureAwait(false); Task debugServerCreation = null; if (creatingDebugServer) { - debugServerCreation = CreateDebugServerWithLanguageServer(languageServer.LanguageServer.Services); + debugServerCreation = CreateDebugServerWithLanguageServer(languageServer); } languageServer.StartAsync(); @@ -90,23 +81,16 @@ public async Task RunUntilShutdown() public void Dispose() { - _loggerFactory.Dispose(); } - private async Task RunTempDebugSession(HostDetails hostDetails, LogLevel minimumLogLevel) + private async Task RunTempDebugSession(HostStartupInfo hostDetails) { - PsesDebugServer debugServer = await CreateDebugServerForTempSession(hostDetails, minimumLogLevel).ConfigureAwait(false); + PsesDebugServer debugServer = await CreateDebugServerForTempSession(hostDetails).ConfigureAwait(false); await debugServer.StartAsync().ConfigureAwait(false); await debugServer.WaitForShutdown().ConfigureAwait(false); return; } - private Task StartDebugServer(IServiceProvider serviceProvider) - { - Task debugServerCreation = CreateDebugServerWithLanguageServer(serviceProvider); - return StartDebugServer(debugServerCreation); - } - private async Task StartDebugServer(Task debugServerCreation) { PsesDebugServer debugServer = await debugServerCreation.ConfigureAwait(false); @@ -119,32 +103,38 @@ private async Task StartDebugServer(Task debugServerCreation) return; } - private async Task CreateLanguageServer(HostDetails hostDetails, LogLevel minimumLogLevel) + private Task RestartDebugServer(PsesDebugServer debugServer) + { + Task debugServerCreation = RecreateDebugServer(debugServer); + return StartDebugServer(debugServerCreation); + } + + private async Task CreateLanguageServer(HostStartupInfo hostDetails) { (Stream inStream, Stream outStream) = await _config.LanguageServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); - return new PsesLanguageServer( - _loggerFactory, - minimumLogLevel, - inStream, - outStream, - _config.FeatureFlags ?? Array.Empty(), - hostDetails, - _config.AdditionalModules ?? Array.Empty()); + return _serverFactory.CreateLanguageServer(inStream, outStream, hostDetails); + } + + private async Task CreateDebugServerWithLanguageServer(PsesLanguageServer languageServer) + { + (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + + return _serverFactory.CreateDebugServerWithLanguageServer(inStream, outStream, languageServer); } - private async Task CreateDebugServerWithLanguageServer(IServiceProvider languageServerServiceProvider) + private async Task RecreateDebugServer(PsesDebugServer debugServer) { (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); - return PsesDebugServer.CreateWithLanguageServerServices(_loggerFactory, inStream, outStream, languageServerServiceProvider); + return _serverFactory.RecreateDebugServer(inStream, outStream, debugServer); } - private async Task CreateDebugServerForTempSession(HostDetails hostDetails, LogLevel minimumLogLevel) + private async Task CreateDebugServerForTempSession(HostStartupInfo hostDetails) { (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); - return PsesDebugServer.CreateForTempSession(_loggerFactory, minimumLogLevel, inStream, outStream, _config.FeatureFlags, hostDetails, _config.AdditionalModules); + return _serverFactory.CreateDebugServerForTempSession(inStream, outStream, hostDetails); } private ProfilePathConfig GetProfilePaths(ProfilePathConfig profilePathConfig) @@ -183,37 +173,12 @@ private ProfilePathConfig GetProfilePaths(ProfilePathConfig profilePathConfig) private void DebugServer_OnSessionEnded(object sender, EventArgs args) { var oldServer = (PsesDebugServer)sender; - IServiceProvider serviceProvider = oldServer.ServiceProvider; oldServer.Dispose(); _alreadySubscribedDebug = false; Task.Run(() => { - StartDebugServer(serviceProvider); + RestartDebugServer(oldServer); }); } - - private static LogLevel ConvertToExtensionLogLevel(PsesLogLevel logLevel) - { - switch (logLevel) - { - case PsesLogLevel.Diagnostic: - return LogLevel.Trace; - - case PsesLogLevel.Verbose: - return LogLevel.Debug; - - case PsesLogLevel.Normal: - return LogLevel.Information; - - case PsesLogLevel.Warning: - return LogLevel.Warning; - - case PsesLogLevel.Error: - return LogLevel.Error; - - default: - return LogLevel.Information; - } - } } } diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index 200de22da..a0ec8ff2d 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -3,10 +3,10 @@ namespace PowerShellEditorServices.Hosting { public enum PsesLogLevel { - Diagnostic, - Verbose, - Normal, - Warning, - Error, + Diagnostic = 0, + Verbose = 1, + Normal = 2, + Warning = 3, + Error = 4, } } diff --git a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj index ca05c356b..efd0f83aa 100644 --- a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj +++ b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj @@ -23,17 +23,18 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + diff --git a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs new file mode 100644 index 000000000..b9a460290 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.Loader; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + internal class PsesLoadContext : AssemblyLoadContext + { + private readonly string _dependencyDirPath; + + public PsesLoadContext(string dependencyDirPath) + { + _dependencyDirPath = dependencyDirPath; + } + + protected override Assembly Load(AssemblyName assemblyName) + { + Console.WriteLine($"Attempting to load {assemblyName} in PSES load context"); + + string asmPath = Path.Combine(_dependencyDirPath, $"{assemblyName.Name}.dll"); + + if (File.Exists(asmPath)) + { + return LoadFromAssemblyPath(asmPath); + } + + return null; + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index a69589fd5..eeea491ac 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -61,7 +61,7 @@ public sealed class StartEditorServicesCommand : PSCmdlet /// [Parameter] [ValidateNotNullOrEmpty] - public string BundledModulePath { get; set; } + public string BundledModulesPath { get; set; } [Parameter] [ValidateNotNullOrEmpty] @@ -90,37 +90,58 @@ public sealed class StartEditorServicesCommand : PSCmdlet protected override void EndProcessing() { - using (var pwsh = SMA.PowerShell.Create()) + if (WaitForDebugger || true) { - bool hasPSReadLine = pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Get-Module", typeof(GetModuleCommand))) - .AddParameter("Name", "PSReadLine") - .Invoke() - .Any(); - - if (hasPSReadLine) + while (!System.Diagnostics.Debugger.IsAttached) { - pwsh.Commands.Clear(); - - pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Remove-Module", typeof(RemoveModuleCommand))) - .AddParameter("Name", "PSReadLine") - .AddParameter("ErrorAction", "SilentlyContinue"); + System.Console.WriteLine($"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}"); + System.Threading.Thread.Sleep(500); } } - var hostInfo = new HostInfo(HostName, HostProfileId, HostVersion); - var editorServicesConfig = new EditorServicesConfig(hostInfo, Host, SessionDetailsPath, BundledModulePath, LogPath) + try { - FeatureFlags = FeatureFlags, - LogLevel = LogLevel, - ConsoleRepl = GetReplKind(), - AdditionalModules = AdditionalModules, - LanguageServiceTransport = GetLanguageServiceTransport(), - DebugServiceTransport = GetDebugServiceTransport(), - }; - - using (var psesLoader = EditorServicesLoader.Create(editorServicesConfig)) + using (var pwsh = SMA.PowerShell.Create()) + { + bool hasPSReadLine = pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Get-Module", typeof(GetModuleCommand))) + .AddParameter("Name", "PSReadLine") + .Invoke() + .Any(); + + if (hasPSReadLine) + { + pwsh.Commands.Clear(); + + pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Remove-Module", typeof(RemoveModuleCommand))) + .AddParameter("Name", "PSReadLine") + .AddParameter("ErrorAction", "SilentlyContinue"); + } + } + + var hostInfo = new HostInfo(HostName, HostProfileId, HostVersion); + var editorServicesConfig = new EditorServicesConfig(hostInfo, Host, SessionDetailsPath, BundledModulesPath, LogPath) + { + FeatureFlags = FeatureFlags, + LogLevel = LogLevel, + ConsoleRepl = GetReplKind(), + AdditionalModules = AdditionalModules, + LanguageServiceTransport = GetLanguageServiceTransport(), + DebugServiceTransport = GetDebugServiceTransport(), + }; + + using (var psesLoader = EditorServicesLoader.Create(editorServicesConfig)) + { + psesLoader.LoadAndRunEditorServicesAsync().Wait(); + } + } + catch (Exception e) { - psesLoader.LoadAndRunEditorServicesAsync().Wait(); + WriteError(new ErrorRecord(e, "PsesLoadError", ErrorCategory.NotSpecified, null)); + + // Give the user a chance to read the message + Console.ReadKey(); + + throw; } } diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs b/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs new file mode 100644 index 000000000..d826b6cae --- /dev/null +++ b/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs @@ -0,0 +1,11 @@ + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + internal static class EditorServicesLoading + { + internal static void LoadEditorServicesForHost() + { + // No op that forces loading this assembly + } + } +} diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs new file mode 100644 index 000000000..7474356b0 --- /dev/null +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -0,0 +1,60 @@ +using Microsoft.Extensions.Logging; +using Microsoft.PowerShell.EditorServices.Server; +using Serilog; +using System.Collections.Generic; +using System.IO; + +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + internal class EditorServicesServerFactory + { + public static EditorServicesServerFactory Create(string logPath, int minimumLogLevel) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.File(logPath) + .MinimumLevel.Verbose() + .CreateLogger(); + + ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + + return new EditorServicesServerFactory(loggerFactory, (LogLevel)minimumLogLevel); + } + + private readonly ILoggerFactory _loggerFactory; + + private readonly Extensions.Logging.ILogger _logger; + + private readonly LogLevel _minimumLogLevel; + + public EditorServicesServerFactory(ILoggerFactory loggerFactory, LogLevel minimumLogLevel) + { + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + _minimumLogLevel = minimumLogLevel; + } + + public PsesLanguageServer CreateLanguageServer( + Stream inputStream, + Stream outputStream, + HostStartupInfo hostDetails) + { + return new PsesLanguageServer(_loggerFactory, _minimumLogLevel, inputStream, outputStream, hostDetails); + } + + public PsesDebugServer CreateDebugServerWithLanguageServer(Stream inputStream, Stream outputStream, PsesLanguageServer languageServer) + { + return PsesDebugServer.CreateWithLanguageServerServices(_loggerFactory, inputStream, outputStream, languageServer.LanguageServer.Services); + } + + public PsesDebugServer RecreateDebugServer(Stream inputStream, Stream outputStream, PsesDebugServer debugServer) + { + return PsesDebugServer.CreateWithLanguageServerServices(_loggerFactory, inputStream, outputStream, debugServer.ServiceProvider); + } + + public PsesDebugServer CreateDebugServerForTempSession(Stream inputStream, Stream outputStream, HostStartupInfo hostStartupInfo) + { + return PsesDebugServer.CreateForTempSession(_loggerFactory, _minimumLogLevel, inputStream, outputStream, hostStartupInfo); + } + } +} diff --git a/src/PowerShellEditorServices/hosting.cs b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs similarity index 83% rename from src/PowerShellEditorServices/hosting.cs rename to src/PowerShellEditorServices/Hosting/HostStartupInfo.cs index 89954ea8a..ea1996dca 100644 --- a/src/PowerShellEditorServices/hosting.cs +++ b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs @@ -1,22 +1,14 @@ -using System; +using System; +using System.Collections.Generic; using System.Management.Automation.Host; -using System.Runtime.CompilerServices; namespace Microsoft.PowerShell.EditorServices.Hosting { - internal static class EditorServicesLoading - { - internal static void LoadEditorServicesForHost() - { - // No op that forces loading this assembly - } - } - /// /// Contains details about the current host application (most /// likely the editor which is using the host process). /// - public class HostDetails + public class HostStartupInfo { #region Constants @@ -39,11 +31,6 @@ public class HostDetails /// public static readonly Version DefaultHostVersion = new Version("0.0.0"); - /// - /// The default host details in a HostDetails object. - /// - public static readonly HostDetails Default = new HostDetails(null, null, null, null, null, null, false, false); - #endregion #region Properties @@ -68,12 +55,20 @@ public class HostDetails public string AllUsersProfilePath { get; } + public IReadOnlyList FeatureFlags { get; } + + public IReadOnlyList AdditionalModules { get; } + public bool ConsoleReplEnabled { get; } public bool UsesLegacyReadLine { get; } public PSHost PSHost { get; } + public string LogPath { get; } + + public int LogLevel { get; } + #endregion #region Constructors @@ -92,13 +87,17 @@ public class HostDetails /// will be used. /// /// The host application's version. - public HostDetails( + public HostStartupInfo( string name, string profileId, Version version, PSHost psHost, string allUsersProfilePath, string currentUsersProfilePath, + IReadOnlyList featureFlags, + IReadOnlyList additionalModules, + string logPath, + int logLevel, bool consoleReplEnabled, bool usesLegacyReadLine) { @@ -108,6 +107,10 @@ public HostDetails( PSHost = psHost; AllUsersProfilePath = allUsersProfilePath; CurrentUserProfilePath = currentUsersProfilePath; + FeatureFlags = featureFlags ?? Array.Empty(); + AdditionalModules = additionalModules ?? Array.Empty(); + LogPath = logPath; + LogLevel = logLevel; ConsoleReplEnabled = consoleReplEnabled; UsesLegacyReadLine = usesLegacyReadLine; } diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 2868ff837..1cb86ecfa 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -29,8 +29,10 @@ + + diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 5e4edb54a..650dfe9ba 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -46,9 +46,7 @@ public static PsesDebugServer CreateForTempSession( LogLevel minimumLogLevel, Stream inputStream, Stream outputStream, - IReadOnlyCollection featureFlags, - HostDetails hostDetails, - IReadOnlyList additionalModules) + HostStartupInfo hostDetails) { var serviceProvider = new ServiceCollection() .AddLogging(builder => builder @@ -56,10 +54,7 @@ public static PsesDebugServer CreateForTempSession( .AddSerilog() .SetMinimumLevel(LogLevel.Trace)) .AddSingleton(provider => null) - .AddPsesLanguageServices( - new HashSet(featureFlags, StringComparer.OrdinalIgnoreCase), - hostDetails, - additionalModules) + .AddPsesLanguageServices(hostDetails) .BuildServiceProvider(); return new PsesDebugServer(loggerFactory, inputStream, outputStream, serviceProvider, useTempSession: true); diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index d2f07c66d..aba06d55a 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -26,9 +26,7 @@ internal class PsesLanguageServer private readonly LogLevel _minimumLogLevel; private readonly Stream _inputStream; private readonly Stream _outputStream; - private readonly HashSet _featureFlags; - private readonly IReadOnlyList _additionalModules; - private readonly HostDetails _hostDetails; + private readonly HostStartupInfo _hostDetails; private readonly TaskCompletionSource _serverStart; public PsesLanguageServer( @@ -36,17 +34,13 @@ public PsesLanguageServer( LogLevel minimumLogLevel, Stream inputStream, Stream outputStream, - IReadOnlyCollection featureFlags, - HostDetails hostDetails, - IReadOnlyList additionalModules) + HostStartupInfo hostDetails) { LoggerFactory = factory; _minimumLogLevel = minimumLogLevel; _inputStream = inputStream; _outputStream = outputStream; - _featureFlags = new HashSet(featureFlags, StringComparer.OrdinalIgnoreCase); _hostDetails = hostDetails; - _additionalModules = additionalModules; _serverStart = new TaskCompletionSource(); } @@ -58,10 +52,7 @@ public async Task StartAsync() .WithInput(_inputStream) .WithOutput(_outputStream) .WithServices(serviceCollection => serviceCollection - .AddPsesLanguageServices( - _featureFlags, - _hostDetails, - _additionalModules)) + .AddPsesLanguageServices(_hostDetails)) .ConfigureLogging(builder => builder .AddSerilog(Log.Logger) .AddLanguageServer(LogLevel.Trace) diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs index d0c7a2c06..150e50c25 100644 --- a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -15,11 +15,9 @@ namespace Microsoft.PowerShell.EditorServices.Server { internal static class PsesServiceCollectionExtensions { - public static IServiceCollection AddPsesLanguageServices ( + public static IServiceCollection AddPsesLanguageServices( this IServiceCollection collection, - HashSet featureFlags, - HostDetails hostDetails, - IReadOnlyList additionalModules) + HostStartupInfo hostDetails) { return collection.AddSingleton() .AddSingleton() @@ -29,9 +27,7 @@ public static IServiceCollection AddPsesLanguageServices ( PowerShellContextService.Create( provider.GetService(), provider.GetService(), - featureFlags, - hostDetails, - additionalModules)) + hostDetails)) .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index 34abea88b..ddc43b150 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -168,9 +168,7 @@ public PowerShellContextService( public static PowerShellContextService Create( ILoggerFactory factory, OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, - HashSet featureFlags, - HostDetails hostDetails, - IReadOnlyList additionalModules) + HostStartupInfo hostDetails) { var logger = factory.CreateLogger(); @@ -211,7 +209,7 @@ public static PowerShellContextService Create( // 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 additionalModules) + foreach (string module in hostDetails.AdditionalModules) { var command = new PSCommand() @@ -238,7 +236,7 @@ public static PowerShellContextService Create( /// An ILogger implementation to use for this instance. /// public static Runspace CreateRunspace( - HostDetails hostDetails, + HostStartupInfo hostDetails, PowerShellContextService powerShellContext, EditorServicesPSHostUserInterface hostUserInterface, ILogger logger) diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs index 7eab3458d..5eda7bd6c 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/EditorServicesPSHost.cs @@ -22,7 +22,7 @@ public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession #region Private Fields private ILogger Logger; - private HostDetails hostDetails; + private HostStartupInfo hostDetails; private Guid instanceId = Guid.NewGuid(); private EditorServicesPSHostUserInterface hostUserInterface; private IHostSupportsInteractiveSession hostSupportsInteractiveSession; @@ -48,7 +48,7 @@ public class EditorServicesPSHost : PSHost, IHostSupportsInteractiveSession /// An ILogger implementation to use for this host. public EditorServicesPSHost( PowerShellContextService powerShellContext, - HostDetails hostDetails, + HostStartupInfo hostDetails, EditorServicesPSHostUserInterface hostUserInterface, ILogger logger) { From 1be54f2f0988cba55435713e07d5b8329736103d Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Mon, 25 Nov 2019 15:39:44 -0800 Subject: [PATCH 06/62] Fix loading issues --- PowerShellEditorServices.build.ps1 | 5 +++ .../EditorServicesRunner.cs | 45 +++++++++---------- .../PowerShellEditorServices.Hosting.csproj | 2 +- .../PsesLoadContext.cs | 4 +- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index a9718ffe4..e6fabc72c 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -279,6 +279,11 @@ task LayoutModule -After Build { $psesDlls = [System.Collections.Generic.HashSet[string]]::new() foreach ($psesComponent in Get-ChildItem $script:PsesOutput) { + if ($psesComponent.Name -eq 'System.Management.Automation.dll') + { + continue + } + if ($psesComponent.Extension -eq '.dll') { [void]$psesDlls.Add($psesComponent.Name) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 59bd33ca9..a14671cf4 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -37,22 +37,7 @@ public async Task RunUntilShutdown() // TODO: Validate config here // Set up information required to instantiate servers - - ProfilePathConfig profilePaths = GetProfilePaths(_config.ProfilePaths); - - var hostStartupInfo = new HostStartupInfo( - _config.HostInfo.Name, - _config.HostInfo.ProfileId, - _config.HostInfo.Version, - _config.PSHost, - profilePaths.AllUsersProfilePath, - profilePaths.CurrentUserProfilePath, - _config.FeatureFlags, - _config.AdditionalModules, - _config.LogPath, - (int)_config.LogLevel, - consoleReplEnabled: _config.ConsoleRepl != ConsoleReplKind.None, - usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine); + HostStartupInfo hostStartupInfo = CreateHostStartupInfo(); if (isTempDebugSession) { @@ -137,17 +122,27 @@ private async Task CreateDebugServerForTempSession(HostStartupI return _serverFactory.CreateDebugServerForTempSession(inStream, outStream, hostDetails); } - private ProfilePathConfig GetProfilePaths(ProfilePathConfig profilePathConfig) + private HostStartupInfo CreateHostStartupInfo() { - string allUsersPath = null; - string currentUserPath = null; + (string allUsersProfilePath, string currentUserProfilePath) = GetProfilePaths(_config.ProfilePaths?.AllUsersProfilePath, _config.ProfilePaths?.CurrentUserProfilePath); - if (profilePathConfig != null) - { - allUsersPath = profilePathConfig.AllUsersProfilePath; - currentUserPath = profilePathConfig.CurrentUserProfilePath; - } + return new HostStartupInfo( + _config.HostInfo.Name, + _config.HostInfo.ProfileId, + _config.HostInfo.Version, + _config.PSHost, + allUsersProfilePath, + currentUserProfilePath, + _config.FeatureFlags, + _config.AdditionalModules, + _config.LogPath, + (int)_config.LogLevel, + consoleReplEnabled: _config.ConsoleRepl != ConsoleReplKind.None, + usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine); + } + private (string allUsersPath, string currentUserPath) GetProfilePaths(string allUsersPath, string currentUserPath) + { if (allUsersPath == null || currentUserPath == null) { using (var pwsh = PowerShell.Create()) @@ -167,7 +162,7 @@ private ProfilePathConfig GetProfilePaths(ProfilePathConfig profilePathConfig) } } - return new ProfilePathConfig(allUsersPath, currentUserPath); + return (allUsersPath, currentUserPath); } private void DebugServer_OnSessionEnded(object sender, EventArgs args) diff --git a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj index efd0f83aa..d5d50fd0d 100644 --- a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj +++ b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj @@ -18,7 +18,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs index b9a460290..bc83162ae 100644 --- a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs +++ b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs @@ -16,15 +16,15 @@ public PsesLoadContext(string dependencyDirPath) protected override Assembly Load(AssemblyName assemblyName) { - Console.WriteLine($"Attempting to load {assemblyName} in PSES load context"); - string asmPath = Path.Combine(_dependencyDirPath, $"{assemblyName.Name}.dll"); if (File.Exists(asmPath)) { + Console.WriteLine($"Loading {assemblyName} in PSES load context"); return LoadFromAssemblyPath(asmPath); } + Console.WriteLine($"Failed to load {assemblyName} in PSES load context"); return null; } } From 23abd872928b0b6c81dc5e2fd7bf330cfcf1fe2f Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Mon, 25 Nov 2019 17:51:26 -0800 Subject: [PATCH 07/62] Write session file --- .../EditorServicesLoader.cs | 14 ++- .../EditorServicesRunner.cs | 27 +++-- .../PowerShellEditorServices.Hosting.csproj | 1 + .../SessionFileWriter.cs | 98 +++++++++++++++++++ .../StartEditorServicesCommand.cs | 38 +++++-- .../TransportConfig.cs | 63 +++++++++--- 6 files changed, 205 insertions(+), 36 deletions(-) create mode 100644 src/PowerShellEditorServices.Hosting/SessionFileWriter.cs diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 5ed9f57d3..be2a1ad63 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -24,7 +24,10 @@ public sealed class EditorServicesLoader : IDisposable private static readonly AssemblyLoadContext s_coreAsmLoadContext = new PsesLoadContext(s_psesDependencyDirPath); #endif - public static EditorServicesLoader Create(EditorServicesConfig hostConfig, string dependencyPath = null) + public static EditorServicesLoader Create( + EditorServicesConfig hostConfig, + ISessionFileWriter sessionFileWriter, + string dependencyPath = null) { #if CoreCLR AssemblyLoadContext.Default.Resolving += DefaultLoadContext_OnAssemblyResolve; @@ -32,14 +35,17 @@ public static EditorServicesLoader Create(EditorServicesConfig hostConfig, strin AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_OnAssemblyResolve; #endif - return new EditorServicesLoader(hostConfig); + return new EditorServicesLoader(hostConfig, sessionFileWriter); } private readonly EditorServicesConfig _hostConfig; - public EditorServicesLoader(EditorServicesConfig hostConfig) + private readonly ISessionFileWriter _sessionFileWriter; + + public EditorServicesLoader(EditorServicesConfig hostConfig, ISessionFileWriter sessionFileWriter) { _hostConfig = hostConfig; + _sessionFileWriter = sessionFileWriter; } public async Task LoadAndRunEditorServicesAsync() @@ -47,7 +53,7 @@ public async Task LoadAndRunEditorServicesAsync() // Method with no implementation that forces the PSES assembly to load, triggering an AssemblyResolve event EditorServicesLoading.LoadEditorServicesForHost(); - using (var editorServicesRunner = EditorServicesRunner.Create(_hostConfig)) + using (var editorServicesRunner = EditorServicesRunner.Create(_hostConfig, _sessionFileWriter)) { await editorServicesRunner.RunUntilShutdown().ConfigureAwait(false); } diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index a14671cf4..2ace87f0d 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -10,25 +10,41 @@ namespace PowerShellEditorServices.Hosting { internal class EditorServicesRunner : IDisposable { - public static EditorServicesRunner Create(EditorServicesConfig config) + public static EditorServicesRunner Create(EditorServicesConfig config, ISessionFileWriter sessionFileWriter) { - return new EditorServicesRunner(config); + return new EditorServicesRunner(config, sessionFileWriter); } private readonly EditorServicesConfig _config; + private readonly ISessionFileWriter _sessionFileWriter; + private readonly EditorServicesServerFactory _serverFactory; private bool _alreadySubscribedDebug; - private EditorServicesRunner(EditorServicesConfig config) + private EditorServicesRunner(EditorServicesConfig config, ISessionFileWriter sessionFileWriter) { _config = config; + _sessionFileWriter = sessionFileWriter; _serverFactory = EditorServicesServerFactory.Create(_config.LogPath, (int)_config.LogLevel); _alreadySubscribedDebug = false; } public async Task RunUntilShutdown() + { + Task runAndAwaitShutdown = CreateEditorServicesAndRunUntilShutdown(); + + _sessionFileWriter.WriteSessionStarted(_config.LanguageServiceTransport, _config.DebugServiceTransport); + + await runAndAwaitShutdown.ConfigureAwait(false); + } + + public void Dispose() + { + } + + private async Task CreateEditorServicesAndRunUntilShutdown() { bool creatingLanguageServer = _config.LanguageServiceTransport != null; bool creatingDebugServer = _config.DebugServiceTransport != null; @@ -61,11 +77,6 @@ public async Task RunUntilShutdown() } await languageServer.WaitForShutdown().ConfigureAwait(false); - return; - } - - public void Dispose() - { } private async Task RunTempDebugSession(HostStartupInfo hostDetails) diff --git a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj index d5d50fd0d..eaf7e9da6 100644 --- a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj +++ b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj @@ -3,6 +3,7 @@ netcoreapp2.1;net461 Microsoft.PowerShell.EditorServices.Hosting + latest diff --git a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs new file mode 100644 index 000000000..88525b913 --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs @@ -0,0 +1,98 @@ +using Microsoft.PowerShell.EditorServices.Hosting; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Management.Automation; +using System.Runtime.InteropServices.ComTypes; +using System.Text; + +namespace PowerShellEditorServices.Hosting +{ + public interface ISessionFileWriter + { + void WriteSessionFailure(string reason, object details); + + void WriteSessionStarted(ITransportConfig languageServiceTransport, ITransportConfig debugAdapterTransport); + } + + internal class SessionFileWriter : ISessionFileWriter + { + private readonly string _sessionFilePath; + + public SessionFileWriter(string sessionFilePath) + { + _sessionFilePath = sessionFilePath; + } + + public void WriteSessionFailure(string reason, object details) + { + var sessionObject = new Dictionary + { + { "status", "failed" }, + { "reason", reason }, + }; + + if (details != null) + { + sessionObject["details"] = details; + } + + WriteSessionObject(sessionObject); + } + + public void WriteSessionStarted(ITransportConfig languageServiceTransport, ITransportConfig debugAdapterTransport) + { + var sessionObject = new Dictionary + { + { "status", "started" }, + { "languageServiceTransport", languageServiceTransport.SessionFileTransportName }, + { "debugServiceTransport", debugAdapterTransport.SessionFileTransportName }, + }; + + if (languageServiceTransport != null) + { + sessionObject["languageServiceTransport"] = languageServiceTransport.SessionFileTransportName; + + if (languageServiceTransport.SessionFileEntries != null) + { + foreach (KeyValuePair sessionEntry in languageServiceTransport.SessionFileEntries) + { + sessionObject[$"languageService{sessionEntry.Key}"] = sessionEntry.Value; + } + } + } + + if (debugAdapterTransport != null) + { + sessionObject["debugServiceTransport"] = debugAdapterTransport.SessionFileTransportName; + + if (debugAdapterTransport.SessionFileEntries != null) + { + foreach (KeyValuePair sessionEntry in debugAdapterTransport.SessionFileEntries) + { + sessionObject[$"debugService{sessionEntry.Key}"] = sessionEntry.Value; + } + } + } + + WriteSessionObject(sessionObject); + } + + private void WriteSessionObject(Dictionary sessionObject) + { + using (var pwsh = PowerShell.Create(RunspaceMode.NewRunspace)) + { + pwsh.AddCommand("ConvertTo-Json") + .AddParameter("InputObject", sessionObject) + .AddParameter("Depth", 10) + .AddParameter("Compress") + .AddCommand("Out-File") + .AddParameter("FilePath", _sessionFilePath) + .AddParameter("Encoding", "utf8") + .AddParameter("Force") + .Invoke(); + } + } + } +} diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index eeea491ac..390e38f42 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -88,16 +88,26 @@ public sealed class StartEditorServicesCommand : PSCmdlet [Parameter] public SwitchParameter WaitForDebugger { get; set; } - protected override void EndProcessing() + [Parameter] + public SwitchParameter SplitInOutPipes { get; set; } + + protected override void BeginProcessing() { +#if DEBUG if (WaitForDebugger || true) { while (!System.Diagnostics.Debugger.IsAttached) { - System.Console.WriteLine($"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}"); + Console.WriteLine($"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}"); System.Threading.Thread.Sleep(500); } } +#endif + } + + protected override void EndProcessing() + { + var sessionFileWriter = new SessionFileWriter(SessionDetailsPath); try { @@ -129,7 +139,7 @@ protected override void EndProcessing() DebugServiceTransport = GetDebugServiceTransport(), }; - using (var psesLoader = EditorServicesLoader.Create(editorServicesConfig)) + using (var psesLoader = EditorServicesLoader.Create(editorServicesConfig, sessionFileWriter)) { psesLoader.LoadAndRunEditorServicesAsync().Wait(); } @@ -172,12 +182,17 @@ private ITransportConfig GetLanguageServiceTransport() return new StdioTransportConfig(); } - if (LanguageServicePipeName != null) + if (LanguageServiceInPipeName != null && LanguageServiceOutPipeName != null) { - return new DuplexNamedPipeTransportConfig(LanguageServicePipeName); + return SimplexNamedPipeTransportConfig.Create(LanguageServiceInPipeName, LanguageServiceOutPipeName); } - return new SimplexNamedPipeTransportConfig(LanguageServiceInPipeName, LanguageServiceOutPipeName); + if (SplitInOutPipes) + { + return SimplexNamedPipeTransportConfig.Create(LanguageServicePipeName); + } + + return DuplexNamedPipeTransportConfig.Create(LanguageServicePipeName); } private ITransportConfig GetDebugServiceTransport() @@ -189,12 +204,17 @@ private ITransportConfig GetDebugServiceTransport() : null; } - if (DebugServicePipeName != null) + if (DebugServiceInPipeName != null && DebugServiceOutPipeName != null) + { + return SimplexNamedPipeTransportConfig.Create(DebugServiceInPipeName, DebugServiceOutPipeName); + } + + if (SplitInOutPipes) { - return new DuplexNamedPipeTransportConfig(DebugServicePipeName); + return SimplexNamedPipeTransportConfig.Create(DebugServicePipeName); } - return new SimplexNamedPipeTransportConfig(DebugServiceInPipeName, DebugServiceOutPipeName); + return DuplexNamedPipeTransportConfig.Create(DebugServicePipeName); } } } diff --git a/src/PowerShellEditorServices.Hosting/TransportConfig.cs b/src/PowerShellEditorServices.Hosting/TransportConfig.cs index a98742bc2..d8b84e237 100644 --- a/src/PowerShellEditorServices.Hosting/TransportConfig.cs +++ b/src/PowerShellEditorServices.Hosting/TransportConfig.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Pipes; using System.Threading.Tasks; @@ -19,7 +20,9 @@ public interface ITransportConfig string Endpoint { get; } - void Validate(); + string SessionFileTransportName { get; } + + IReadOnlyDictionary SessionFileEntries { get; } } public class StdioTransportConfig : ITransportConfig @@ -28,56 +31,90 @@ public class StdioTransportConfig : ITransportConfig public string Endpoint => ""; + public string SessionFileTransportName => "Stdio"; + + public IReadOnlyDictionary SessionFileEntries { get; } = null; + public Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync() { return Task.FromResult((Console.OpenStandardInput(), Console.OpenStandardOutput())); } - - public void Validate() - { - } } public class DuplexNamedPipeTransportConfig : ITransportConfig { + public static DuplexNamedPipeTransportConfig Create(string pipeName) + { + return new DuplexNamedPipeTransportConfig(pipeName ?? NamedPipeUtils.GenerateValidNamedPipeName()); + } + private readonly string _pipeName; - public DuplexNamedPipeTransportConfig(string pipeName) + private DuplexNamedPipeTransportConfig(string pipeName) { _pipeName = pipeName; + SessionFileEntries = new Dictionary{ { "PipeName", pipeName } }; } public string Endpoint => $"InOut pipe: {_pipeName}"; public TransportType TransportType => TransportType.NamedPipe; + public string SessionFileTransportName => "NamedPipe"; + + public IReadOnlyDictionary SessionFileEntries { get; } + public async Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync() { NamedPipeServerStream namedPipe = NamedPipeUtils.CreateNamedPipe(_pipeName, PipeDirection.InOut); await namedPipe.WaitForConnectionAsync().ConfigureAwait(false); return (namedPipe, namedPipe); } - - public void Validate() - { - } } public class SimplexNamedPipeTransportConfig : ITransportConfig { + public static SimplexNamedPipeTransportConfig Create(string pipeNameBase) + { + if (pipeNameBase == null) + { + pipeNameBase = NamedPipeUtils.GenerateValidNamedPipeName(); + } + + string inPipeName = $"in_{pipeNameBase}"; + string outPipeName = $"out_{pipeNameBase}"; + + return SimplexNamedPipeTransportConfig.Create(inPipeName, outPipeName); + } + + public static SimplexNamedPipeTransportConfig Create(string inPipeName, string outPipeName) + { + return new SimplexNamedPipeTransportConfig(inPipeName, outPipeName); + } + private readonly string _inPipeName; private readonly string _outPipeName; - public SimplexNamedPipeTransportConfig(string inPipeName, string outPipeName) + private SimplexNamedPipeTransportConfig(string inPipeName, string outPipeName) { _inPipeName = inPipeName; _outPipeName = outPipeName; + + SessionFileEntries = new Dictionary + { + { "ReadPipeName", inPipeName }, + { "WritePipeName", outPipeName }, + }; } public string Endpoint => $"In pipe: {_inPipeName} Out pipe: {_outPipeName}"; public TransportType TransportType => TransportType.NamedPipe; + public string SessionFileTransportName => "NamedPipeSimplex"; + + public IReadOnlyDictionary SessionFileEntries { get; } + public async Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync() { NamedPipeServerStream inPipe = NamedPipeUtils.CreateNamedPipe(_inPipeName, PipeDirection.InOut); @@ -90,9 +127,5 @@ public SimplexNamedPipeTransportConfig(string inPipeName, string outPipeName) return (inPipe, outPipe); } - - public void Validate() - { - } } } From e50e95cc7d83caad8b73a413e1177d9b149d0968 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 26 Nov 2019 08:56:14 -0800 Subject: [PATCH 08/62] Fix startup connection issue --- .../EditorServicesLoader.cs | 2 +- src/PowerShellEditorServices.Hosting/PsesLoadContext.cs | 4 ++-- .../StartEditorServicesCommand.cs | 2 +- src/PowerShellEditorServices.Hosting/TransportConfig.cs | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index be2a1ad63..408eb6c0e 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -67,7 +67,7 @@ public void Dispose() #if CoreCLR private static Assembly DefaultLoadContext_OnAssemblyResolve(AssemblyLoadContext defaultLoadContext, AssemblyName asmName) { - Console.WriteLine($".NET Core resolving {asmName}"); + //Console.WriteLine($".NET Core resolving {asmName}"); if (!string.Equals(asmName.Name, "Microsoft.PowerShell.EditorServices", StringComparison.Ordinal)) { diff --git a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs index bc83162ae..4788b2777 100644 --- a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs +++ b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs @@ -20,11 +20,11 @@ protected override Assembly Load(AssemblyName assemblyName) if (File.Exists(asmPath)) { - Console.WriteLine($"Loading {assemblyName} in PSES load context"); + //Console.WriteLine($"Loading {assemblyName} in PSES load context"); return LoadFromAssemblyPath(asmPath); } - Console.WriteLine($"Failed to load {assemblyName} in PSES load context"); + //Console.WriteLine($"Failed to load {assemblyName} in PSES load context"); return null; } } diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 390e38f42..80d9d731d 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -94,7 +94,7 @@ public sealed class StartEditorServicesCommand : PSCmdlet protected override void BeginProcessing() { #if DEBUG - if (WaitForDebugger || true) + if (WaitForDebugger) { while (!System.Diagnostics.Debugger.IsAttached) { diff --git a/src/PowerShellEditorServices.Hosting/TransportConfig.cs b/src/PowerShellEditorServices.Hosting/TransportConfig.cs index d8b84e237..69ab90ce6 100644 --- a/src/PowerShellEditorServices.Hosting/TransportConfig.cs +++ b/src/PowerShellEditorServices.Hosting/TransportConfig.cs @@ -53,7 +53,7 @@ public static DuplexNamedPipeTransportConfig Create(string pipeName) private DuplexNamedPipeTransportConfig(string pipeName) { _pipeName = pipeName; - SessionFileEntries = new Dictionary{ { "PipeName", pipeName } }; + SessionFileEntries = new Dictionary{ { "PipeName", NamedPipeUtils.GetNamedPipePath(pipeName) } }; } public string Endpoint => $"InOut pipe: {_pipeName}"; @@ -102,8 +102,8 @@ private SimplexNamedPipeTransportConfig(string inPipeName, string outPipeName) SessionFileEntries = new Dictionary { - { "ReadPipeName", inPipeName }, - { "WritePipeName", outPipeName }, + { "ReadPipeName", NamedPipeUtils.GetNamedPipePath(inPipeName) }, + { "WritePipeName", NamedPipeUtils.GetNamedPipePath(outPipeName) }, }; } From 884a7425252cb667bcd2e2c5e8add89a18cbcdc6 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 26 Nov 2019 12:32:34 -0800 Subject: [PATCH 09/62] Add logging --- .editorconfig | 3 + .../EditorServicesLoader.cs | 18 +- .../EditorServicesRunner.cs | 28 +++- .../HostLogger.cs | 157 +++++++++++++++++- .../SessionFileWriter.cs | 21 ++- .../StartEditorServicesCommand.cs | 42 ++++- 6 files changed, 246 insertions(+), 23 deletions(-) diff --git a/.editorconfig b/.editorconfig index 335885163..1dd0a2a9e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,3 +21,6 @@ trim_trailing_whitespace = true [*.{ps1xml,props,xml,yaml}] indent_size = 2 + +# CA1303: Do not pass literals as localized parameters +dotnet_diagnostic.CA1303.severity = none diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 408eb6c0e..53416ad1c 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -25,25 +25,31 @@ public sealed class EditorServicesLoader : IDisposable #endif public static EditorServicesLoader Create( + HostLogger logger, EditorServicesConfig hostConfig, ISessionFileWriter sessionFileWriter, string dependencyPath = null) { #if CoreCLR + logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for new AssemblyLoadContext dependency loading"); AssemblyLoadContext.Default.Resolving += DefaultLoadContext_OnAssemblyResolve; #else + logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for dependency loading"); AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_OnAssemblyResolve; #endif - return new EditorServicesLoader(hostConfig, sessionFileWriter); + return new EditorServicesLoader(logger, hostConfig, sessionFileWriter); } private readonly EditorServicesConfig _hostConfig; private readonly ISessionFileWriter _sessionFileWriter; - public EditorServicesLoader(EditorServicesConfig hostConfig, ISessionFileWriter sessionFileWriter) + private readonly HostLogger _logger; + + public EditorServicesLoader(HostLogger logger, EditorServicesConfig hostConfig, ISessionFileWriter sessionFileWriter) { + _logger = logger; _hostConfig = hostConfig; _sessionFileWriter = sessionFileWriter; } @@ -51,9 +57,11 @@ public EditorServicesLoader(EditorServicesConfig hostConfig, ISessionFileWriter public async Task LoadAndRunEditorServicesAsync() { // Method with no implementation that forces the PSES assembly to load, triggering an AssemblyResolve event + _logger.Log(PsesLogLevel.Verbose, "Loading PSES assemblies"); EditorServicesLoading.LoadEditorServicesForHost(); - using (var editorServicesRunner = EditorServicesRunner.Create(_hostConfig, _sessionFileWriter)) + _logger.Log(PsesLogLevel.Verbose, "Starting EditorServices"); + using (var editorServicesRunner = EditorServicesRunner.Create(_logger, _hostConfig, _sessionFileWriter)) { await editorServicesRunner.RunUntilShutdown().ConfigureAwait(false); } @@ -67,8 +75,6 @@ public void Dispose() #if CoreCLR private static Assembly DefaultLoadContext_OnAssemblyResolve(AssemblyLoadContext defaultLoadContext, AssemblyName asmName) { - //Console.WriteLine($".NET Core resolving {asmName}"); - if (!string.Equals(asmName.Name, "Microsoft.PowerShell.EditorServices", StringComparison.Ordinal)) { return null; @@ -83,8 +89,6 @@ private static Assembly DefaultLoadContext_OnAssemblyResolve(AssemblyLoadContext #if !CoreCLR private static Assembly CurrentDomain_OnAssemblyResolve(object sender, ResolveEventArgs args) { - Console.WriteLine($".NET FX resolving {args.Name}"); - var asmName = new AssemblyName(args.Name); string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 2ace87f0d..52e492e24 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -10,11 +10,13 @@ namespace PowerShellEditorServices.Hosting { internal class EditorServicesRunner : IDisposable { - public static EditorServicesRunner Create(EditorServicesConfig config, ISessionFileWriter sessionFileWriter) + public static EditorServicesRunner Create(HostLogger logger, EditorServicesConfig config, ISessionFileWriter sessionFileWriter) { - return new EditorServicesRunner(config, sessionFileWriter); + return new EditorServicesRunner(logger, config, sessionFileWriter); } + private readonly HostLogger _logger; + private readonly EditorServicesConfig _config; private readonly ISessionFileWriter _sessionFileWriter; @@ -23,8 +25,9 @@ public static EditorServicesRunner Create(EditorServicesConfig config, ISessionF private bool _alreadySubscribedDebug; - private EditorServicesRunner(EditorServicesConfig config, ISessionFileWriter sessionFileWriter) + private EditorServicesRunner(HostLogger logger, EditorServicesConfig config, ISessionFileWriter sessionFileWriter) { + _logger = logger; _config = config; _sessionFileWriter = sessionFileWriter; _serverFactory = EditorServicesServerFactory.Create(_config.LogPath, (int)_config.LogLevel); @@ -35,6 +38,7 @@ public async Task RunUntilShutdown() { Task runAndAwaitShutdown = CreateEditorServicesAndRunUntilShutdown(); + _logger.Log(PsesLogLevel.Diagnostic, "Writing session file"); _sessionFileWriter.WriteSessionStarted(_config.LanguageServiceTransport, _config.DebugServiceTransport); await runAndAwaitShutdown.ConfigureAwait(false); @@ -81,8 +85,11 @@ private async Task CreateEditorServicesAndRunUntilShutdown() private async Task RunTempDebugSession(HostStartupInfo hostDetails) { + _logger.Log(PsesLogLevel.Diagnostic, "Running temp debug session"); PsesDebugServer debugServer = await CreateDebugServerForTempSession(hostDetails).ConfigureAwait(false); + _logger.Log(PsesLogLevel.Verbose, "Debug server created"); await debugServer.StartAsync().ConfigureAwait(false); + _logger.Log(PsesLogLevel.Verbose, "Debug server started"); await debugServer.WaitForShutdown().ConfigureAwait(false); return; } @@ -92,37 +99,46 @@ private async Task StartDebugServer(Task debugServerCreation) PsesDebugServer debugServer = await debugServerCreation.ConfigureAwait(false); if (!_alreadySubscribedDebug) { + _logger.Log(PsesLogLevel.Diagnostic, "Subscribing debug server for session ended event"); _alreadySubscribedDebug = true; debugServer.SessionEnded += DebugServer_OnSessionEnded; } + _logger.Log(PsesLogLevel.Diagnostic, "Starting debug server"); debugServer.StartAsync(); return; } private Task RestartDebugServer(PsesDebugServer debugServer) { + _logger.Log(PsesLogLevel.Diagnostic, "Restarting debug server"); Task debugServerCreation = RecreateDebugServer(debugServer); return StartDebugServer(debugServerCreation); } private async Task CreateLanguageServer(HostStartupInfo hostDetails) { + _logger.Log(PsesLogLevel.Verbose, $"Creating LSP transport with endpoint {_config.LanguageServiceTransport.Endpoint}"); (Stream inStream, Stream outStream) = await _config.LanguageServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + _logger.Log(PsesLogLevel.Diagnostic, "Creating language server"); return _serverFactory.CreateLanguageServer(inStream, outStream, hostDetails); } private async Task CreateDebugServerWithLanguageServer(PsesLanguageServer languageServer) { + _logger.Log(PsesLogLevel.Verbose, $"Creating debug adapter transport with endpoint {_config.DebugServiceTransport.Endpoint}"); (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + _logger.Log(PsesLogLevel.Diagnostic, "Creating debug adapter"); return _serverFactory.CreateDebugServerWithLanguageServer(inStream, outStream, languageServer); } private async Task RecreateDebugServer(PsesDebugServer debugServer) { + _logger.Log(PsesLogLevel.Diagnostic, "Recreating debug adapter transport"); (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); + _logger.Log(PsesLogLevel.Diagnostic, "Recreating debug adapter"); return _serverFactory.RecreateDebugServer(inStream, outStream, debugServer); } @@ -135,6 +151,8 @@ private async Task CreateDebugServerForTempSession(HostStartupI private HostStartupInfo CreateHostStartupInfo() { + _logger.Log(PsesLogLevel.Diagnostic, "Creating startup info object"); + (string allUsersProfilePath, string currentUserProfilePath) = GetProfilePaths(_config.ProfilePaths?.AllUsersProfilePath, _config.ProfilePaths?.CurrentUserProfilePath); return new HostStartupInfo( @@ -154,10 +172,13 @@ private HostStartupInfo CreateHostStartupInfo() private (string allUsersPath, string currentUserPath) GetProfilePaths(string allUsersPath, string currentUserPath) { + _logger.Log(PsesLogLevel.Diagnostic, "Configuring profile paths"); + if (allUsersPath == null || currentUserPath == null) { using (var pwsh = PowerShell.Create()) { + _logger.Log(PsesLogLevel.Diagnostic, "Querying PowerShell for profile paths"); Collection profiles = pwsh.AddScript("$profile.AllUsersAllHosts,$profile.CurrentUserAllHosts") .Invoke(); @@ -178,6 +199,7 @@ private HostStartupInfo CreateHostStartupInfo() private void DebugServer_OnSessionEnded(object sender, EventArgs args) { + _logger.Log(PsesLogLevel.Verbose, "Debug session ended. Restarting debug service"); var oldServer = (PsesDebugServer)sender; oldServer.Dispose(); _alreadySubscribedDebug = false; diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index a0ec8ff2d..915838796 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -1,5 +1,11 @@ - -namespace PowerShellEditorServices.Hosting +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Host; +using System.Runtime.CompilerServices; + +namespace Microsoft.PowerShell.EditorServices.Hosting { public enum PsesLogLevel { @@ -9,4 +15,151 @@ public enum PsesLogLevel Warning = 3, Error = 4, } + + public class HostLogger : + IObservable<(PsesLogLevel logLevel, string message)>, + IObservable<(int logLevel, string message)> + { + private struct LogObserver : IObserver<(PsesLogLevel logLevel, string message)> + { + private readonly IObserver<(int logLevel, string message)> _observer; + + public LogObserver(IObserver<(int logLevel, string message)> observer) + { + _observer = observer; + } + + public void OnCompleted() + { + _observer.OnCompleted(); + } + + public void OnError(Exception error) + { + _observer.OnError(error); + } + + public void OnNext((PsesLogLevel logLevel, string message) value) + { + _observer.OnNext(((int)value.logLevel, value.message)); + } + } + + private struct Unsubscriber : IDisposable + { + public void Dispose() + { + } + } + + private readonly PsesLogLevel _minimumLogLevel; + + private readonly ConcurrentQueue<(PsesLogLevel logLevel, string message)> _logMessages; + + private readonly ConcurrentBag> _observers; + + public HostLogger(PsesLogLevel minimumLogLevel) + { + _minimumLogLevel = minimumLogLevel; + _logMessages = new ConcurrentQueue<(PsesLogLevel logLevel, string message)>(); + _observers = new ConcurrentBag>(); + } + + public IDisposable Subscribe(IObserver<(PsesLogLevel logLevel, string message)> observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + _observers.Add(observer); + + foreach ((PsesLogLevel logLevel, string message) entry in _logMessages) + { + observer.OnNext(entry); + } + + return new Unsubscriber(); + } + + public IDisposable Subscribe(IObserver<(int logLevel, string message)> observer) + { + if (observer == null) + { + throw new ArgumentNullException(nameof(observer)); + } + + return Subscribe(new LogObserver(observer)); + } + + public void Log(PsesLogLevel logLevel, string message) + { + if (logLevel < _minimumLogLevel) + { + return; + } + + _logMessages.Enqueue((logLevel, message)); + foreach (IObserver<(PsesLogLevel logLevel, string message)> observer in _observers) + { + observer.OnNext((logLevel, message)); + } + } + + public void LogException( + string message, + Exception exception, + [CallerMemberName] string callerName = null, + [CallerFilePath] string callerSourceFile = null, + [CallerLineNumber] int callerLineNumber = -1) + { + Log(PsesLogLevel.Error, $"{message}. Exception logged in {callerSourceFile} on line {callerLineNumber} in {callerName}:\n{exception}"); + } + + } + + internal class PSHostLogger : IObserver<(PsesLogLevel logLevel, string message)> + { + private readonly PSHostUserInterface _ui; + + public PSHostLogger(PSHostUserInterface ui) + { + _ui = ui; + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + OnNext((PsesLogLevel.Error, $"Error occurred while logging: {error}")); + } + + public void OnNext((PsesLogLevel logLevel, string message) value) + { + switch (value.logLevel) + { + case PsesLogLevel.Diagnostic: + _ui.WriteDebugLine(value.message); + return; + + case PsesLogLevel.Verbose: + _ui.WriteVerboseLine(value.message); + return; + + case PsesLogLevel.Normal: + _ui.WriteLine(value.message); + return; + + case PsesLogLevel.Warning: + _ui.WriteWarningLine(value.message); + return; + + case PsesLogLevel.Error: + _ui.WriteErrorLine(value.message); + return; + } + } + } } diff --git a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs index 88525b913..98c311ebd 100644 --- a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs +++ b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs @@ -18,15 +18,20 @@ public interface ISessionFileWriter internal class SessionFileWriter : ISessionFileWriter { + private HostLogger _logger; + private readonly string _sessionFilePath; - public SessionFileWriter(string sessionFilePath) + public SessionFileWriter(HostLogger logger, string sessionFilePath) { + _logger = logger; _sessionFilePath = sessionFilePath; } public void WriteSessionFailure(string reason, object details) { + _logger.Log(PsesLogLevel.Diagnostic, "Writing session failure"); + var sessionObject = new Dictionary { { "status", "failed" }, @@ -43,6 +48,8 @@ public void WriteSessionFailure(string reason, object details) public void WriteSessionStarted(ITransportConfig languageServiceTransport, ITransportConfig debugAdapterTransport) { + _logger.Log(PsesLogLevel.Diagnostic, "Writing session started"); + var sessionObject = new Dictionary { { "status", "started" }, @@ -81,18 +88,22 @@ public void WriteSessionStarted(ITransportConfig languageServiceTransport, ITran private void WriteSessionObject(Dictionary sessionObject) { + string content = null; using (var pwsh = PowerShell.Create(RunspaceMode.NewRunspace)) { - pwsh.AddCommand("ConvertTo-Json") + content = pwsh.AddCommand("ConvertTo-Json") .AddParameter("InputObject", sessionObject) .AddParameter("Depth", 10) .AddParameter("Compress") - .AddCommand("Out-File") - .AddParameter("FilePath", _sessionFilePath) + .AddCommand("Set-Content") + .AddParameter("Path", _sessionFilePath) .AddParameter("Encoding", "utf8") .AddParameter("Force") - .Invoke(); + .AddParameter("PassThru") + .Invoke()[0]; } + + _logger.Log(PsesLogLevel.Verbose, $"Session file written to {_sessionFilePath} with content:\n{content}"); } } } diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 80d9d731d..7836db3a1 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -10,6 +10,8 @@ namespace Microsoft.PowerShell.EditorServices.Hosting [Cmdlet(VerbsLifecycle.Start, "EditorServices", DefaultParameterSetName = "NamedPipe")] public sealed class StartEditorServicesCommand : PSCmdlet { + private HostLogger _logger; + /// /// The name of the EditorServices host to report /// @@ -103,14 +105,24 @@ protected override void BeginProcessing() } } #endif + + _logger = new HostLogger(LogLevel); + _logger.Subscribe(new PSHostLogger(Host.UI)); + + _logger.Log(PsesLogLevel.Diagnostic, "Logger created"); } protected override void EndProcessing() { - var sessionFileWriter = new SessionFileWriter(SessionDetailsPath); + _logger.Log(PsesLogLevel.Diagnostic, "Beginning EndProcessing block"); + + var sessionFileWriter = new SessionFileWriter(_logger, SessionDetailsPath); + + _logger.Log(PsesLogLevel.Diagnostic, "Session file writer created"); try { + _logger.Log(PsesLogLevel.Verbose, "Removing PSReadLine"); using (var pwsh = SMA.PowerShell.Create()) { bool hasPSReadLine = pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Get-Module", typeof(GetModuleCommand))) @@ -125,9 +137,12 @@ protected override void EndProcessing() pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Remove-Module", typeof(RemoveModuleCommand))) .AddParameter("Name", "PSReadLine") .AddParameter("ErrorAction", "SilentlyContinue"); + + _logger.Log(PsesLogLevel.Verbose, "Removed PSReadLine"); } } + _logger.Log(PsesLogLevel.Diagnostic, "Creating host configuration"); var hostInfo = new HostInfo(HostName, HostProfileId, HostVersion); var editorServicesConfig = new EditorServicesConfig(hostInfo, Host, SessionDetailsPath, BundledModulesPath, LogPath) { @@ -139,14 +154,15 @@ protected override void EndProcessing() DebugServiceTransport = GetDebugServiceTransport(), }; - using (var psesLoader = EditorServicesLoader.Create(editorServicesConfig, sessionFileWriter)) + _logger.Log(PsesLogLevel.Verbose, "Loading EditorServices"); + using (var psesLoader = EditorServicesLoader.Create(_logger, editorServicesConfig, sessionFileWriter)) { psesLoader.LoadAndRunEditorServicesAsync().Wait(); } } catch (Exception e) { - WriteError(new ErrorRecord(e, "PsesLoadError", ErrorCategory.NotSpecified, null)); + _logger.LogException("Exception encountered starting EditorServices", e); // Give the user a chance to read the message Console.ReadKey(); @@ -157,23 +173,31 @@ protected override void EndProcessing() private ConsoleReplKind GetReplKind() { + _logger.Log(PsesLogLevel.Diagnostic, "Determining REPL kind"); + if (Stdio || !EnableConsoleRepl) { + _logger.Log(PsesLogLevel.Diagnostic, "REPL configured as None"); return ConsoleReplKind.None; } if (UseLegacyReadLine) { + _logger.Log(PsesLogLevel.Diagnostic, "REPL configured as Legacy"); return ConsoleReplKind.LegacyReadLine; } + _logger.Log(PsesLogLevel.Diagnostic, "REPL configured as PSReadLine"); return ConsoleReplKind.PSReadLine; } private ITransportConfig GetLanguageServiceTransport() { + _logger.Log(PsesLogLevel.Diagnostic, "Configuring LSP transport"); + if (DebugServiceOnly) { + _logger.Log(PsesLogLevel.Diagnostic, "No LSP transport: PSES is debug only"); return null; } @@ -197,11 +221,17 @@ private ITransportConfig GetLanguageServiceTransport() private ITransportConfig GetDebugServiceTransport() { + _logger.Log(PsesLogLevel.Diagnostic, "Configuring debug transport"); + if (Stdio) { - return DebugServiceOnly - ? new StdioTransportConfig() - : null; + if (DebugServiceOnly) + { + return new StdioTransportConfig(); + } + + _logger.Log(PsesLogLevel.Diagnostic, "No debug transport: Transport is Stdio with debug disabled"); + return null; } if (DebugServiceInPipeName != null && DebugServiceOutPipeName != null) From 3ab36d5a11e6b5f76203940a7c1da9a9d6071bf4 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 26 Nov 2019 16:54:13 -0800 Subject: [PATCH 10/62] Add doc comments + some cleanup --- .../EditorServicesConfig.cs | 91 +++++++++- .../EditorServicesLoader.cs | 35 +++- .../EditorServicesRunner.cs | 47 +++++- .../HostInfo.cs | 27 ++- .../HostLogger.cs | 69 +++++++- .../NamedPipeUtils.cs | 65 +++++++- .../ProfilePathConfig.cs | 19 --- .../PsesLoadContext.cs | 14 +- .../SessionFileWriter.cs | 54 +++++- .../StartEditorServicesCommand.cs | 155 +++++++++++++----- .../TransportConfig.cs | 95 ++++++++--- .../Hosting/EditorServicesLoading.cs | 4 + .../Hosting/EditorServicesServerFactory.cs | 47 +++++- .../Hosting/HostStartupInfo.cs | 43 ++++- .../Server/PsesDebugServer.cs | 24 ++- .../Server/PsesLanguageServer.cs | 23 ++- 16 files changed, 687 insertions(+), 125 deletions(-) delete mode 100644 src/PowerShellEditorServices.Hosting/ProfilePathConfig.cs diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs index 7b4360540..ad1ca2ceb 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs @@ -1,20 +1,40 @@ -using PowerShellEditorServices.Hosting; -using System; +// +// 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.Management.Automation.Host; -using System.Text; namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// Describes the desired console REPL for the integrated console. + /// public enum ConsoleReplKind { + /// No console REPL - there will be no interactive console available. None = 0, + /// Use a REPL with the legacy readline implementation. This is generally used when PSReadLine is unavailable. LegacyReadLine, + /// Use a REPL with the PSReadLine module for console interaction. PSReadLine, } + /// + /// Configuration for editor services startup. + /// public class EditorServicesConfig { + /// + /// Create a new editor services config object, + /// with all required fields. + /// + /// The host description object. + /// The PowerShell host to use in Editor Services. + /// The path to use for the session details file. + /// The path to the modules bundled with Editor Services. + /// The path to be used for Editor Services' logging. public EditorServicesConfig( HostInfo hostInfo, PSHost psHost, @@ -29,28 +49,93 @@ public EditorServicesConfig( LogPath = logPath; } + /// + /// The host description object. + /// public HostInfo HostInfo { get; } + /// + /// The PowerShell host used by Editor Services. + /// public PSHost PSHost { get; } + /// + /// The path to use for the session details file. + /// public string SessionDetailsPath { get; } + /// + /// The path to the modules bundled with EditorServices. + /// public string BundledModulePath { get; } + /// + /// The path to use for logging for Editor Services. + /// public string LogPath { get; } + /// + /// Names of or paths to any additional modules to load on startup. + /// public IReadOnlyList AdditionalModules { get; set; } = null; + /// + /// Flags of features to enable on startup. + /// public IReadOnlyList FeatureFlags { get; set; } = null; + /// + /// The console REPL experience to use in the integrated console + /// (including none to disable the integrated console). + /// public ConsoleReplKind ConsoleRepl { get; set; } = ConsoleReplKind.None; + /// + /// The minimum log level to log events with. + /// public PsesLogLevel LogLevel { get; set; } = PsesLogLevel.Normal; + /// + /// Configuration for the language server protocol transport to use. + /// public ITransportConfig LanguageServiceTransport { get; set; } = null; + /// + /// Configuration for the debug adapter protocol transport to use. + /// public ITransportConfig DebugServiceTransport { get; set; } = null; + /// + /// PowerShell profile locations for Editor Services to use for its profiles. + /// If none are provided, these will be generated from the hosting PowerShell's profile paths. + /// public ProfilePathConfig ProfilePaths { get; set; } = null; } + + /// + /// Configuration for Editor Services' PowerShell profile paths. + /// + public class ProfilePathConfig + { + /// + /// Create a new profile path configuration. + /// + /// The shared profile path. + /// The single user profile path. + public ProfilePathConfig(string allUsersPath, string currentUserPath) + { + AllUsersProfilePath = allUsersPath; + CurrentUserProfilePath = currentUserPath; + } + + /// + /// The path to the profile shared by all users. + /// + public string AllUsersProfilePath { get; } + + /// + /// The path to the profile specific to the current user. + /// + public string CurrentUserProfilePath { get; } + } } diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 53416ad1c..61b2fea0b 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -1,4 +1,8 @@ -using Microsoft.PowerShell.EditorServices.Hosting; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.IO; using System.Reflection; @@ -8,8 +12,13 @@ using System.Runtime.Loader; #endif -namespace PowerShellEditorServices.Hosting +namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// Class to contain the loading behavior of Editor Services. + /// In particular, this class wraps the point where Editor Services is safely loaded + /// in a way that separates its dependencies from the calling context. + /// public sealed class EditorServicesLoader : IDisposable { private const int Net461Version = 394254; @@ -24,16 +33,24 @@ public sealed class EditorServicesLoader : IDisposable private static readonly AssemblyLoadContext s_coreAsmLoadContext = new PsesLoadContext(s_psesDependencyDirPath); #endif + /// + /// Create a new Editor Services loader. + /// + /// The host logger to use. + /// The host configuration to start editor services with. + /// The session file writer to write the session file with. + /// public static EditorServicesLoader Create( HostLogger logger, EditorServicesConfig hostConfig, - ISessionFileWriter sessionFileWriter, - string dependencyPath = null) + ISessionFileWriter sessionFileWriter) { #if CoreCLR + // In .NET Core, we add an event here to redirect dependency loading to the new AssemblyLoadContext we load PSES' dependencies into logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for new AssemblyLoadContext dependency loading"); AssemblyLoadContext.Default.Resolving += DefaultLoadContext_OnAssemblyResolve; #else + // In .NET Framework we add an event here to redirect dependency loading in the current AppDomain for PSES' dependencies logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for dependency loading"); AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_OnAssemblyResolve; #endif @@ -47,13 +64,18 @@ public static EditorServicesLoader Create( private readonly HostLogger _logger; - public EditorServicesLoader(HostLogger logger, EditorServicesConfig hostConfig, ISessionFileWriter sessionFileWriter) + private EditorServicesLoader(HostLogger logger, EditorServicesConfig hostConfig, ISessionFileWriter sessionFileWriter) { _logger = logger; _hostConfig = hostConfig; _sessionFileWriter = sessionFileWriter; } + /// + /// Load Editor Services and its dependencies in an isolated way and start it. + /// This method's returned task will end when Editor Services shuts down. + /// + /// public async Task LoadAndRunEditorServicesAsync() { // Method with no implementation that forces the PSES assembly to load, triggering an AssemblyResolve event @@ -63,6 +85,8 @@ public async Task LoadAndRunEditorServicesAsync() _logger.Log(PsesLogLevel.Verbose, "Starting EditorServices"); using (var editorServicesRunner = EditorServicesRunner.Create(_logger, _hostConfig, _sessionFileWriter)) { + // The trigger method for Editor Services + // We will wait here until Editor Services shuts down await editorServicesRunner.RunUntilShutdown().ConfigureAwait(false); } } @@ -75,6 +99,7 @@ public void Dispose() #if CoreCLR private static Assembly DefaultLoadContext_OnAssemblyResolve(AssemblyLoadContext defaultLoadContext, AssemblyName asmName) { + // We only want the Editor Services DLL; the new ALC will lazily load its dependencies automatically if (!string.Equals(asmName.Name, "Microsoft.PowerShell.EditorServices", StringComparison.Ordinal)) { return null; diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 52e492e24..9ccf9db24 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -1,15 +1,30 @@ -using Microsoft.PowerShell.EditorServices.Hosting; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using Microsoft.PowerShell.EditorServices.Server; using System; using System.Collections.ObjectModel; using System.IO; -using System.Management.Automation; using System.Threading.Tasks; +using SMA = System.Management.Automation; -namespace PowerShellEditorServices.Hosting +namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// Class to manage the startup of PowerShell Editor Services. + /// This should be called by only after Editor Services has been loaded. + /// internal class EditorServicesRunner : IDisposable { + /// + /// Create a new Editor Services runner. + /// + /// The host logger to log through. + /// The startup configuration to use. + /// The session file writer to use. + /// public static EditorServicesRunner Create(HostLogger logger, EditorServicesConfig config, ISessionFileWriter sessionFileWriter) { return new EditorServicesRunner(logger, config, sessionFileWriter); @@ -34,13 +49,20 @@ private EditorServicesRunner(HostLogger logger, EditorServicesConfig config, ISe _alreadySubscribedDebug = false; } + /// + /// Start and run Editor Services and then wait for shutdown. + /// + /// A task that ends when Editor Services shuts down. public async Task RunUntilShutdown() { + // Start Editor Services Task runAndAwaitShutdown = CreateEditorServicesAndRunUntilShutdown(); + // Now write the session file _logger.Log(PsesLogLevel.Diagnostic, "Writing session file"); _sessionFileWriter.WriteSessionStarted(_config.LanguageServiceTransport, _config.DebugServiceTransport); + // Finally, wait for Editor Services to shut down await runAndAwaitShutdown.ConfigureAwait(false); } @@ -48,6 +70,10 @@ public void Dispose() { } + /// + /// Master method for instantiating, running and waiting for the LSP and debug servers at the heart of Editor Services. + /// + /// A task that ends when Editor Services shuts down. private async Task CreateEditorServicesAndRunUntilShutdown() { bool creatingLanguageServer = _config.LanguageServiceTransport != null; @@ -59,12 +85,21 @@ private async Task CreateEditorServicesAndRunUntilShutdown() // Set up information required to instantiate servers HostStartupInfo hostStartupInfo = CreateHostStartupInfo(); + // If we just want a temp debug session, run that and do nothing else if (isTempDebugSession) { await RunTempDebugSession(hostStartupInfo).ConfigureAwait(false); return; } + // We want LSP and maybe debugging + // To do that we: + // - Create the LSP server + // - Possibly kick off the debug server creation + // - Start the LSP server + // - Possibly start the debug server + // - Wait for the LSP server to finish + PsesLanguageServer languageServer = await CreateLanguageServer(hostStartupInfo).ConfigureAwait(false); Task debugServerCreation = null; @@ -97,12 +132,16 @@ private async Task RunTempDebugSession(HostStartupInfo hostDetails) private async Task StartDebugServer(Task debugServerCreation) { PsesDebugServer debugServer = await debugServerCreation.ConfigureAwait(false); + + // When the debug server shuts down, we want it to automatically restart + // To do this, we set an event to allow it to create a new debug server as its session ends if (!_alreadySubscribedDebug) { _logger.Log(PsesLogLevel.Diagnostic, "Subscribing debug server for session ended event"); _alreadySubscribedDebug = true; debugServer.SessionEnded += DebugServer_OnSessionEnded; } + _logger.Log(PsesLogLevel.Diagnostic, "Starting debug server"); debugServer.StartAsync(); return; @@ -176,7 +215,7 @@ private HostStartupInfo CreateHostStartupInfo() if (allUsersPath == null || currentUserPath == null) { - using (var pwsh = PowerShell.Create()) + using (var pwsh = SMA.PowerShell.Create()) { _logger.Log(PsesLogLevel.Diagnostic, "Querying PowerShell for profile paths"); Collection profiles = pwsh.AddScript("$profile.AllUsersAllHosts,$profile.CurrentUserAllHosts") diff --git a/src/PowerShellEditorServices.Hosting/HostInfo.cs b/src/PowerShellEditorServices.Hosting/HostInfo.cs index b8f647a08..465f7ef0d 100644 --- a/src/PowerShellEditorServices.Hosting/HostInfo.cs +++ b/src/PowerShellEditorServices.Hosting/HostInfo.cs @@ -1,11 +1,23 @@ -using System; -using System.Collections.Generic; -using System.Text; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// A simple readonly object to describe basic host metadata. + /// public class HostInfo { + /// + /// Create a new host info object. + /// + /// The name of the host. + /// The profile ID of the host. + /// The version of the host. public HostInfo(string name, string profileId, Version version) { Name = name; @@ -13,10 +25,19 @@ public HostInfo(string name, string profileId, Version version) Version = version; } + /// + /// The name of the host. + /// public string Name { get; } + /// + /// The profile ID of the host. + /// public string ProfileId { get; } + /// + /// The version of the host. + /// public Version Version { get; } } } diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index 915838796..e0e514515 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -1,12 +1,18 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Management.Automation; using System.Management.Automation.Host; using System.Runtime.CompilerServices; namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// User-facing log level for editor services configuration. + /// public enum PsesLogLevel { Diagnostic = 0, @@ -16,10 +22,17 @@ public enum PsesLogLevel Error = 4, } + /// + /// A logging front-end for host startup allowing handover to the backend + /// and decoupling from the host's particular logging sink. + /// public class HostLogger : IObservable<(PsesLogLevel logLevel, string message)>, IObservable<(int logLevel, string message)> { + /// + /// A simple translation struct to convert PsesLogLevel to an int for backend passthrough. + /// private struct LogObserver : IObserver<(PsesLogLevel logLevel, string message)> { private readonly IObserver<(int logLevel, string message)> _observer; @@ -45,6 +58,11 @@ public void OnNext((PsesLogLevel logLevel, string message) value) } } + /// + /// Empty implementation for subscribers to unsubscribe from logging output. + /// Since this logger will stop being called soon after startup, + /// we don't really need to implement unsubscription logic (which can be a bit complicated). + /// private struct Unsubscriber : IDisposable { public void Dispose() @@ -58,6 +76,10 @@ public void Dispose() private readonly ConcurrentBag> _observers; + /// + /// Construct a new logger in the host. + /// + /// The minimum log level to log. public HostLogger(PsesLogLevel minimumLogLevel) { _minimumLogLevel = minimumLogLevel; @@ -65,6 +87,11 @@ public HostLogger(PsesLogLevel minimumLogLevel) _observers = new ConcurrentBag>(); } + /// + /// Subscribe a new log sink. + /// + /// The log sink to subscribe. + /// A disposable unsubscribe object. public IDisposable Subscribe(IObserver<(PsesLogLevel logLevel, string message)> observer) { if (observer == null) @@ -74,6 +101,7 @@ public IDisposable Subscribe(IObserver<(PsesLogLevel logLevel, string message)> _observers.Add(observer); + // Catch up a late subscriber to messages already logged foreach ((PsesLogLevel logLevel, string message) entry in _logMessages) { observer.OnNext(entry); @@ -82,6 +110,11 @@ public IDisposable Subscribe(IObserver<(PsesLogLevel logLevel, string message)> return new Unsubscriber(); } + /// + /// Subscribe a new log sink. + /// + /// The log sink to subscribe. + /// A disposable unsubscribe object. public IDisposable Subscribe(IObserver<(int logLevel, string message)> observer) { if (observer == null) @@ -92,20 +125,37 @@ public IDisposable Subscribe(IObserver<(int logLevel, string message)> observer) return Subscribe(new LogObserver(observer)); } + /// + /// Log a message to log sinks. + /// + /// The log severity level of message to log. + /// The message to log. public void Log(PsesLogLevel logLevel, string message) { + // Do nothing if the severity is lower than the minimum if (logLevel < _minimumLogLevel) { return; } + // Remember this for later subscriptions _logMessages.Enqueue((logLevel, message)); + + // Send this log to all observers foreach (IObserver<(PsesLogLevel logLevel, string message)> observer in _observers) { observer.OnNext((logLevel, message)); } } + /// + /// Convenience method for logging exceptions. + /// + /// The human-directed message to accompany the exception. + /// The actual execption to log. + /// The name of the calling method. + /// The name of the file where this is logged. + /// The line in the file where this is logged. public void LogException( string message, Exception exception, @@ -118,10 +168,23 @@ public void LogException( } + /// + /// A log sink to direct log messages back to the PowerShell host. + /// + /// + /// Note that calling this through the cmdlet causes an error, + /// so instead we log directly to the host. + /// Since it's likely that the process will end when PSES shuts down, + /// there's no good reason to need objects rather than writing directly to the host. + /// internal class PSHostLogger : IObserver<(PsesLogLevel logLevel, string message)> { private readonly PSHostUserInterface _ui; + /// + /// Create a new PowerShell host logger. + /// + /// The PowerShell host user interface object to log output to. public PSHostLogger(PSHostUserInterface ui) { _ui = ui; diff --git a/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs b/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs index 02c71618c..e9dab1969 100644 --- a/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs +++ b/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs @@ -1,17 +1,23 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.Collections.Generic; using System.IO; using System.IO.Pipes; using System.Runtime.InteropServices; -using System.Security.AccessControl; -using System.Security.Principal; -using System.Text; namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// Utility class for handling named pipe creation in .NET Core and .NET Framework. + /// internal static class NamedPipeUtils { #if !CoreCLR + // .NET Framework requires the buffer size to be specified private const int PipeBufferSize = 1024; #endif @@ -28,6 +34,8 @@ internal static NamedPipeServerStream CreateNamedPipe( options: PipeOptions.CurrentUserOnly | PipeOptions.Asynchronous); #else + // In .NET Framework, we must manually ACL the named pipes we create + var pipeSecurity = new PipeSecurity(); WindowsIdentity identity = WindowsIdentity.GetCurrent(); @@ -36,9 +44,10 @@ internal static NamedPipeServerStream CreateNamedPipe( if (principal.IsInRole(WindowsBuiltInRole.Administrator)) { // Allow the Administrators group full access to the pipe. - pipeSecurity.AddAccessRule(new PipeAccessRule( - new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, domainSid: null).Translate(typeof(NTAccount)), - PipeAccessRights.FullControl, AccessControlType.Allow)); + pipeSecurity.AddAccessRule( + new PipeAccessRule( + new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, domainSid: null).Translate(typeof(NTAccount)), + PipeAccessRights.FullControl, AccessControlType.Allow)); } else { @@ -60,14 +69,42 @@ internal static NamedPipeServerStream CreateNamedPipe( #endif } - public static string GenerateValidNamedPipeName() + /// + /// Generate a named pipe name known to not already be in use. + /// + /// Prefix variants of the pipename to test, if any. + /// A named pipe name or name suffix that is safe to you. + public static string GenerateValidNamedPipeName(IReadOnlyCollection prefixes = null) { int tries = 0; do { string pipeName = $"PSES_{Path.GetRandomFileName()}"; - if (IsPipeNameValid(pipeName)) + // In the simple prefix-less case, just test the pipe name + if (prefixes == null) + { + if (!IsPipeNameValid(pipeName)) + { + continue; + } + + return pipeName; + } + + // If we have prefixes, test that all prefix/pipename combinations are valid + bool allPipeNamesValid = true; + foreach (string prefix in prefixes) + { + string prefixedPipeName = $"{prefix}_{pipeName}"; + if (!IsPipeNameValid(prefixedPipeName)) + { + allPipeNamesValid = false; + break; + } + } + + if (allPipeNamesValid) { return pipeName; } @@ -77,6 +114,11 @@ public static string GenerateValidNamedPipeName() throw new Exception("Unable to create named pipe; no available names"); } + /// + /// Validate that a named pipe file name is a legitimate named pipe file name and is not already in use. + /// + /// The named pipe name to validate. This should be a simple name rather than a path. + /// True if the named pipe name is valid, false otherwise. public static bool IsPipeNameValid(string pipeName) { if (string.IsNullOrEmpty(pipeName)) @@ -87,6 +129,11 @@ public static bool IsPipeNameValid(string pipeName) return !File.Exists(GetNamedPipePath(pipeName)); } + /// + /// Get the path of a named pipe given its name. + /// + /// The simple name of the named pipe. + /// The full path of the named pipe. public static string GetNamedPipePath(string pipeName) { #if CoreCLR diff --git a/src/PowerShellEditorServices.Hosting/ProfilePathConfig.cs b/src/PowerShellEditorServices.Hosting/ProfilePathConfig.cs deleted file mode 100644 index e4faaffa5..000000000 --- a/src/PowerShellEditorServices.Hosting/ProfilePathConfig.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace PowerShellEditorServices.Hosting -{ - public class ProfilePathConfig - { - public ProfilePathConfig(string allUsersPath, string currentUserPath) - { - AllUsersProfilePath = allUsersPath; - CurrentUserProfilePath = currentUserPath; - } - - public string AllUsersProfilePath { get; } - - public string CurrentUserProfilePath { get; } - } -} diff --git a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs index 4788b2777..ccd5563ce 100644 --- a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs +++ b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs @@ -1,10 +1,20 @@ -using System; +// +// 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.Reflection; using System.Runtime.Loader; namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// An AssemblyLoadContext (ALC) designed to find PSES' dependencies in the given directory. + /// This class only exists in .NET Core, where the ALC is used to isolate PSES' dependencies + /// from the PowerShell assembly load context so that modules can import their own dependencies + /// without issue in PSES. + /// internal class PsesLoadContext : AssemblyLoadContext { private readonly string _dependencyDirPath; @@ -20,11 +30,9 @@ protected override Assembly Load(AssemblyName assemblyName) if (File.Exists(asmPath)) { - //Console.WriteLine($"Loading {assemblyName} in PSES load context"); return LoadFromAssemblyPath(asmPath); } - //Console.WriteLine($"Failed to load {assemblyName} in PSES load context"); return null; } } diff --git a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs index 98c311ebd..6756da13a 100644 --- a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs +++ b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs @@ -1,33 +1,60 @@ -using Microsoft.PowerShell.EditorServices.Hosting; -using System; -using System.Collections; +// +// 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.Diagnostics; using System.Management.Automation; -using System.Runtime.InteropServices.ComTypes; -using System.Text; +using SMA = System.Management.Automation; -namespace PowerShellEditorServices.Hosting +namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// Writes the session file when the server is ready for a connection, + /// so that the client can connect. + /// public interface ISessionFileWriter { + /// + /// Write a session file describing a failed startup. + /// + /// The reason for the startup failure. + /// Any details to accompany the reason. void WriteSessionFailure(string reason, object details); + /// + /// Write a session file describing a successful startup. + /// + /// The transport configuration for the LSP service. + /// The transport configuration for the debug adapter service. void WriteSessionStarted(ITransportConfig languageServiceTransport, ITransportConfig debugAdapterTransport); } - internal class SessionFileWriter : ISessionFileWriter + /// + /// The default session file writer, which uses PowerShell to write a session file. + /// + public class SessionFileWriter : ISessionFileWriter { private HostLogger _logger; private readonly string _sessionFilePath; + /// + /// Construct a new session file writer for the given session file path. + /// + /// The logger to log actions with. + /// The path to write the session file path to. public SessionFileWriter(HostLogger logger, string sessionFilePath) { _logger = logger; _sessionFilePath = sessionFilePath; } + /// + /// Write a startup failure to the session file. + /// + /// The reason for the startup failure. + /// Any extra details, which will be serialized as JSON. public void WriteSessionFailure(string reason, object details) { _logger.Log(PsesLogLevel.Diagnostic, "Writing session failure"); @@ -46,6 +73,11 @@ public void WriteSessionFailure(string reason, object details) WriteSessionObject(sessionObject); } + /// + /// Write a successful server startup to the session file. + /// + /// The LSP service transport configuration. + /// The debug adapter transport configuration. public void WriteSessionStarted(ITransportConfig languageServiceTransport, ITransportConfig debugAdapterTransport) { _logger.Log(PsesLogLevel.Diagnostic, "Writing session started"); @@ -86,10 +118,14 @@ public void WriteSessionStarted(ITransportConfig languageServiceTransport, ITran WriteSessionObject(sessionObject); } + /// + /// Write the object representing the session file to the file by serializing it as JSON. + /// + /// The dictionary representing the session file. private void WriteSessionObject(Dictionary sessionObject) { string content = null; - using (var pwsh = PowerShell.Create(RunspaceMode.NewRunspace)) + using (var pwsh = SMA.PowerShell.Create(RunspaceMode.NewRunspace)) { content = pwsh.AddCommand("ConvertTo-Json") .AddParameter("InputObject", sessionObject) diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 7836db3a1..96585befa 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -1,5 +1,9 @@ -using Microsoft.PowerShell.Commands; -using PowerShellEditorServices.Hosting; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.PowerShell.Commands; using System; using System.Linq; using System.Management.Automation; @@ -7,6 +11,9 @@ namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services. + /// [Cmdlet(VerbsLifecycle.Start, "EditorServices", DefaultParameterSetName = "NamedPipe")] public sealed class StartEditorServicesCommand : PSCmdlet { @@ -33,28 +40,56 @@ public sealed class StartEditorServicesCommand : PSCmdlet [ValidateNotNullOrEmpty] public Version HostVersion { get; set; } + /// + /// Path to the session file to create on startup or startup failure. + /// [Parameter(Mandatory = true)] [ValidateNotNullOrEmpty] public string SessionDetailsPath { get; set; } + /// + /// The name of the named pipe to use for the LSP transport. + /// If left unset and named pipes are used as transport, a new name will be generated. + /// [Parameter(ParameterSetName = "NamedPipe")] public string LanguageServicePipeName { get; set; } + /// + /// The name of the named pipe to use for the debug adapter transport. + /// If left unset and named pipes are used as a transport, a new name will be generated. + /// [Parameter(ParameterSetName = "NamedPipe")] public string DebugServicePipeName { get; set; } + /// + /// The name of the input named pipe to use for the LSP transport. + /// [Parameter(ParameterSetName = "NamedPipeSimplex")] public string LanguageServiceInPipeName { get; set; } + /// + /// The name of the output named pipe to use for the LSP transport. + /// [Parameter(ParameterSetName = "NamedPipeSimplex")] public string LanguageServiceOutPipeName { get; set; } + /// + /// The name of the input pipe to use for the debug adapter transport. + /// [Parameter(ParameterSetName = "NamedPipeSimplex")] public string DebugServiceInPipeName { get; set; } + /// + /// The name of the output pipe to use for the debug adapter transport. + /// [Parameter(ParameterSetName = "NamedPipeSimplex")] public string DebugServiceOutPipeName { get; set; } + /// + /// If set, uses standard input/output as the LSP transport. + /// When is set with this, standard input/output + /// is used as the debug adapter transport. + /// [Parameter(ParameterSetName = "Stdio")] public SwitchParameter Stdio { get; set; } @@ -65,31 +100,59 @@ public sealed class StartEditorServicesCommand : PSCmdlet [ValidateNotNullOrEmpty] public string BundledModulesPath { get; set; } + /// + /// The absolute path to the where the editor services log file should be created and logged to. + /// [Parameter] [ValidateNotNullOrEmpty] public string LogPath { get; set; } + /// + /// The minimum log level that should be emitted. + /// [Parameter] public PsesLogLevel LogLevel { get; set; } + /// + /// Paths to additional PowerShell modules to be imported at startup. + /// [Parameter] public string[] AdditionalModules { get; set; } + /// + /// Any feature flags to enable in EditorServices. + /// [Parameter] public string[] FeatureFlags { get; set; } + /// + /// When set, enables the integrated console. + /// [Parameter] public SwitchParameter EnableConsoleRepl { get; set; } + /// + /// When set and the console is enabled, the legacy lightweight + /// readline implementation will be used instead of PSReadLine. + /// [Parameter] public SwitchParameter UseLegacyReadLine { get; set; } + /// + /// When set, do not enable LSP service, only the debug adapter. + /// [Parameter] public SwitchParameter DebugServiceOnly { get; set; } + /// + /// When set with a debug build, startup will wait for a debugger to attach. + /// [Parameter] public SwitchParameter WaitForDebugger { get; set; } + /// + /// When set, will generate two simplex named pipes using a single named pipe name. + /// [Parameter] public SwitchParameter SplitInOutPipes { get; set; } @@ -101,14 +164,14 @@ protected override void BeginProcessing() while (!System.Diagnostics.Debugger.IsAttached) { Console.WriteLine($"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}"); - System.Threading.Thread.Sleep(500); + System.Threading.Thread.Sleep(1000); } } #endif + // Set up logging now for use throughout startup _logger = new HostLogger(LogLevel); _logger.Subscribe(new PSHostLogger(Host.UI)); - _logger.Log(PsesLogLevel.Diagnostic, "Logger created"); } @@ -116,47 +179,21 @@ protected override void EndProcessing() { _logger.Log(PsesLogLevel.Diagnostic, "Beginning EndProcessing block"); - var sessionFileWriter = new SessionFileWriter(_logger, SessionDetailsPath); - - _logger.Log(PsesLogLevel.Diagnostic, "Session file writer created"); - try { - _logger.Log(PsesLogLevel.Verbose, "Removing PSReadLine"); - using (var pwsh = SMA.PowerShell.Create()) - { - bool hasPSReadLine = pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Get-Module", typeof(GetModuleCommand))) - .AddParameter("Name", "PSReadLine") - .Invoke() - .Any(); + // First try to remove PSReadLine to decomplicate startup + // If PSReadLine is enabled, it will be re-imported later + RemovePSReadLineForStartup(); - if (hasPSReadLine) - { - pwsh.Commands.Clear(); + // Create the configuration from parameters + EditorServicesConfig editorServicesConfig = CreateConfigObject(); - pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Remove-Module", typeof(RemoveModuleCommand))) - .AddParameter("Name", "PSReadLine") - .AddParameter("ErrorAction", "SilentlyContinue"); + var sessionFileWriter = new SessionFileWriter(_logger, SessionDetailsPath); + _logger.Log(PsesLogLevel.Diagnostic, "Session file writer created"); - _logger.Log(PsesLogLevel.Verbose, "Removed PSReadLine"); - } - } - - _logger.Log(PsesLogLevel.Diagnostic, "Creating host configuration"); - var hostInfo = new HostInfo(HostName, HostProfileId, HostVersion); - var editorServicesConfig = new EditorServicesConfig(hostInfo, Host, SessionDetailsPath, BundledModulesPath, LogPath) - { - FeatureFlags = FeatureFlags, - LogLevel = LogLevel, - ConsoleRepl = GetReplKind(), - AdditionalModules = AdditionalModules, - LanguageServiceTransport = GetLanguageServiceTransport(), - DebugServiceTransport = GetDebugServiceTransport(), - }; - - _logger.Log(PsesLogLevel.Verbose, "Loading EditorServices"); using (var psesLoader = EditorServicesLoader.Create(_logger, editorServicesConfig, sessionFileWriter)) { + _logger.Log(PsesLogLevel.Verbose, "Loading EditorServices"); psesLoader.LoadAndRunEditorServicesAsync().Wait(); } } @@ -167,10 +204,50 @@ protected override void EndProcessing() // Give the user a chance to read the message Console.ReadKey(); - throw; + ThrowTerminatingError(new ErrorRecord(e, "PowerShellEditorServicesError", ErrorCategory.NotSpecified, this)); } } + private void RemovePSReadLineForStartup() + { + _logger.Log(PsesLogLevel.Verbose, "Removing PSReadLine"); + using (var pwsh = SMA.PowerShell.Create()) + { + bool hasPSReadLine = pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Get-Module", typeof(GetModuleCommand))) + .AddParameter("Name", "PSReadLine") + .Invoke() + .Any(); + + if (hasPSReadLine) + { + pwsh.Commands.Clear(); + + pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Remove-Module", typeof(RemoveModuleCommand))) + .AddParameter("Name", "PSReadLine") + .AddParameter("ErrorAction", "SilentlyContinue"); + + _logger.Log(PsesLogLevel.Verbose, "Removed PSReadLine"); + } + } + } + + private EditorServicesConfig CreateConfigObject() + { + _logger.Log(PsesLogLevel.Diagnostic, "Creating host configuration"); + var hostInfo = new HostInfo(HostName, HostProfileId, HostVersion); + var editorServicesConfig = new EditorServicesConfig(hostInfo, Host, SessionDetailsPath, BundledModulesPath, LogPath) + { + FeatureFlags = FeatureFlags, + LogLevel = LogLevel, + ConsoleRepl = GetReplKind(), + AdditionalModules = AdditionalModules, + LanguageServiceTransport = GetLanguageServiceTransport(), + DebugServiceTransport = GetDebugServiceTransport(), + }; + + return editorServicesConfig; + } + private ConsoleReplKind GetReplKind() { _logger.Log(PsesLogLevel.Diagnostic, "Determining REPL kind"); diff --git a/src/PowerShellEditorServices.Hosting/TransportConfig.cs b/src/PowerShellEditorServices.Hosting/TransportConfig.cs index 69ab90ce6..19fcad13f 100644 --- a/src/PowerShellEditorServices.Hosting/TransportConfig.cs +++ b/src/PowerShellEditorServices.Hosting/TransportConfig.cs @@ -1,4 +1,9 @@ -using System; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; using System.Collections.Generic; using System.IO; using System.IO.Pipes; @@ -6,29 +11,38 @@ namespace Microsoft.PowerShell.EditorServices.Hosting { - public enum TransportType - { - Stdio, - NamedPipe, - } - + /// + /// Configuration specifying an editor services protocol transport stream configuration. + /// public interface ITransportConfig { + /// + /// Create, connect and return the configured transport streams. + /// + /// The connected transport streams. inStream and outStream may be the same stream for duplex streams. Task<(Stream inStream, Stream outStream)> ConnectStreamsAsync(); - TransportType TransportType { get; } - + /// + /// The name of the transport endpoint for logging. + /// string Endpoint { get; } + /// + /// The name of the transport to record in the session file. + /// string SessionFileTransportName { get; } + /// + /// Extra entries to record in the session file. + /// IReadOnlyDictionary SessionFileEntries { get; } } + /// + /// Configuration for the standard input/output transport. + /// public class StdioTransportConfig : ITransportConfig { - public TransportType TransportType => TransportType.Stdio; - public string Endpoint => ""; public string SessionFileTransportName => "Stdio"; @@ -41,11 +55,32 @@ public class StdioTransportConfig : ITransportConfig } } + /// + /// Configuration for a full duplex named pipe. + /// public class DuplexNamedPipeTransportConfig : ITransportConfig { + /// + /// Create a duplex named pipe transport config with an automatically generated pipe name. + /// + /// A new duplex named pipe transport configuration. + public static DuplexNamedPipeTransportConfig Create() + { + return new DuplexNamedPipeTransportConfig(NamedPipeUtils.GenerateValidNamedPipeName()); + } + + /// + /// Create a duplex named pipe transport config with the given pipe name. + /// + /// A new duplex named pipe transport configuration. public static DuplexNamedPipeTransportConfig Create(string pipeName) { - return new DuplexNamedPipeTransportConfig(pipeName ?? NamedPipeUtils.GenerateValidNamedPipeName()); + if (pipeName == null) + { + return DuplexNamedPipeTransportConfig.Create(); + } + + return new DuplexNamedPipeTransportConfig(pipeName); } private readonly string _pipeName; @@ -58,8 +93,6 @@ private DuplexNamedPipeTransportConfig(string pipeName) public string Endpoint => $"InOut pipe: {_pipeName}"; - public TransportType TransportType => TransportType.NamedPipe; - public string SessionFileTransportName => "NamedPipe"; public IReadOnlyDictionary SessionFileEntries { get; } @@ -72,21 +105,47 @@ private DuplexNamedPipeTransportConfig(string pipeName) } } + /// + /// Configuration for two simplex named pipes. + /// public class SimplexNamedPipeTransportConfig : ITransportConfig { + private static readonly IReadOnlyList s_pipeNamePrefixes = new[] + { + "in", + "out", + }; + + /// + /// Create a pair of simplex named pipes using generated names. + /// + /// A new simplex named pipe transport config. + public static SimplexNamedPipeTransportConfig Create() + { + return SimplexNamedPipeTransportConfig.Create(NamedPipeUtils.GenerateValidNamedPipeName(s_pipeNamePrefixes)); + } + + /// + /// Create a pair of simplex named pipes using the given name as a base. + /// + /// A new simplex named pipe transport config. public static SimplexNamedPipeTransportConfig Create(string pipeNameBase) { if (pipeNameBase == null) { - pipeNameBase = NamedPipeUtils.GenerateValidNamedPipeName(); + return SimplexNamedPipeTransportConfig.Create(); } - string inPipeName = $"in_{pipeNameBase}"; - string outPipeName = $"out_{pipeNameBase}"; + string inPipeName = $"{s_pipeNamePrefixes[0]}_{pipeNameBase}"; + string outPipeName = $"{s_pipeNamePrefixes[1]}_{pipeNameBase}"; return SimplexNamedPipeTransportConfig.Create(inPipeName, outPipeName); } + /// + /// Create a pair of simplex named pipes using the given names. + /// + /// A new simplex named pipe transport config. public static SimplexNamedPipeTransportConfig Create(string inPipeName, string outPipeName) { return new SimplexNamedPipeTransportConfig(inPipeName, outPipeName); @@ -109,8 +168,6 @@ private SimplexNamedPipeTransportConfig(string inPipeName, string outPipeName) public string Endpoint => $"In pipe: {_inPipeName} Out pipe: {_outPipeName}"; - public TransportType TransportType => TransportType.NamedPipe; - public string SessionFileTransportName => "NamedPipeSimplex"; public IReadOnlyDictionary SessionFileEntries { get; } diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs b/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs index d826b6cae..0578a6089 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs @@ -1,6 +1,10 @@ namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// Implementation-free class designed to safely allow PowerShell Editor Services to be loaded in an obvious way. + /// Referencing this class will force looking for and loading the PSES assembly if it's not already loaded. + /// internal static class EditorServicesLoading { internal static void LoadEditorServicesForHost() diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index 7474356b0..141aa39f6 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -6,8 +6,21 @@ namespace Microsoft.PowerShell.EditorServices.Hosting { + /// + /// Factory class for hiding dependencies of Editor Services. + /// In particular, dependency injection and logging are wrapped by factory methods on this class + /// so that the host assembly can construct the LSP and debug servers + /// without taking logging or dependency injection dependencies directly. + /// internal class EditorServicesServerFactory { + /// + /// Create a new Editor Services factory. + /// This method will instantiate logging. + /// + /// The path of the log file to use. + /// The minimum log level to use. + /// public static EditorServicesServerFactory Create(string logPath, int minimumLogLevel) { Log.Logger = new LoggerConfiguration() @@ -27,34 +40,62 @@ public static EditorServicesServerFactory Create(string logPath, int minimumLogL private readonly LogLevel _minimumLogLevel; - public EditorServicesServerFactory(ILoggerFactory loggerFactory, LogLevel minimumLogLevel) + private EditorServicesServerFactory(ILoggerFactory loggerFactory, LogLevel minimumLogLevel) { _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); _minimumLogLevel = minimumLogLevel; } + /// + /// Create the LSP server. + /// + /// The protocol transport input stream. + /// The protocol transport output stream. + /// The host details configuration for Editor Services instantation. + /// A new, unstarted language server instance. public PsesLanguageServer CreateLanguageServer( Stream inputStream, Stream outputStream, HostStartupInfo hostDetails) { - return new PsesLanguageServer(_loggerFactory, _minimumLogLevel, inputStream, outputStream, hostDetails); + return new PsesLanguageServer(_loggerFactory, inputStream, outputStream, hostDetails); } + /// + /// Create the debug server given a language server instance. + /// + /// The protocol transport input stream. + /// The protocol transport output stream. + /// + /// A new, unstarted debug server instance. public PsesDebugServer CreateDebugServerWithLanguageServer(Stream inputStream, Stream outputStream, PsesLanguageServer languageServer) { return PsesDebugServer.CreateWithLanguageServerServices(_loggerFactory, inputStream, outputStream, languageServer.LanguageServer.Services); } + /// + /// Create a new debug server based on an old one in an ended session. + /// + /// The protocol transport input stream. + /// The protocol transport output stream. + /// The old debug server to recreate. + /// public PsesDebugServer RecreateDebugServer(Stream inputStream, Stream outputStream, PsesDebugServer debugServer) { return PsesDebugServer.CreateWithLanguageServerServices(_loggerFactory, inputStream, outputStream, debugServer.ServiceProvider); } + /// + /// Create a standalone debug server for temp sessions. + /// + /// The protocol transport input stream. + /// The protocol transport output stream. + /// The host startup configuration to create the server session with. + /// public PsesDebugServer CreateDebugServerForTempSession(Stream inputStream, Stream outputStream, HostStartupInfo hostStartupInfo) { - return PsesDebugServer.CreateForTempSession(_loggerFactory, _minimumLogLevel, inputStream, outputStream, hostStartupInfo); + return PsesDebugServer.CreateForTempSession(_loggerFactory, inputStream, outputStream, hostStartupInfo); } } } diff --git a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs index ea1996dca..43b392d3b 100644 --- a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs +++ b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs @@ -5,10 +5,10 @@ namespace Microsoft.PowerShell.EditorServices.Hosting { /// - /// Contains details about the current host application (most - /// likely the editor which is using the host process). + /// Contains details about the host as well as any other information + /// needed by Editor Services at startup time. /// - public class HostStartupInfo + internal class HostStartupInfo { #region Constants @@ -51,22 +51,50 @@ public class HostStartupInfo /// public Version Version { get; } + /// + /// The path to the single user PowerShell profile. + /// public string CurrentUserProfilePath { get; } + /// + /// The path to the shared PowerShell profile. + /// public string AllUsersProfilePath { get; } + /// + /// Any feature flags enabled at startup. + /// public IReadOnlyList FeatureFlags { get; } + /// + /// Names or paths of any additional modules to import on startup. + /// public IReadOnlyList AdditionalModules { get; } + /// + /// True if the integrated console is to be enabled. + /// public bool ConsoleReplEnabled { get; } + /// + /// If true, the legacy PSES readline implementation will be used. Otherwise PSReadLine will be used. + /// If the console REPL is not enabled, this setting will be ignored. + /// public bool UsesLegacyReadLine { get; } + /// + /// The PowerShell host to use with Editor Services. + /// public PSHost PSHost { get; } + /// + /// The path of the log file Editor Services should log to. + /// public string LogPath { get; } + /// + /// The minimum log level of log events to be logged. + /// public int LogLevel { get; } #endregion @@ -87,6 +115,15 @@ public class HostStartupInfo /// will be used. /// /// The host application's version. + /// The PowerShell host to use. + /// The path to the shared profile. + /// The path to the user specific profile. + /// Flags of features to enable. + /// Names or paths of additional modules to import. + /// The path to log to. + /// The minimum log event level. + /// Enable console if true. + /// Use PSReadLine if false, otherwise use the legacy readline implementation. public HostStartupInfo( string name, string profileId, diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 650dfe9ba..14eb3af94 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -19,6 +19,9 @@ namespace Microsoft.PowerShell.EditorServices.Server { + /// + /// Server for hosting debug sessions. + /// internal class PsesDebugServer : IDisposable { protected readonly ILoggerFactory _loggerFactory; @@ -32,6 +35,14 @@ internal class PsesDebugServer : IDisposable private readonly TaskCompletionSource _serverStopped; + /// + /// Create a debug server using existing services from a language server instance. + /// + /// Factory to instantiate loggers with. + /// Protocol transport input stream. + /// Protocol transport output stream. + /// Service provider from the language server to use. + /// public static PsesDebugServer CreateWithLanguageServerServices( ILoggerFactory loggerFactory, Stream inputStream, @@ -41,9 +52,16 @@ public static PsesDebugServer CreateWithLanguageServerServices( return new PsesDebugServer(loggerFactory, inputStream, outputStream, languageServerServiceProvider, useTempSession: false); } + /// + /// Create a debug server instantiating services from a host configuration. + /// + /// Factory to create loggers with. + /// Protocol transport input stream. + /// Protocol transport output stream. + /// The host configuration to create services with. + /// public static PsesDebugServer CreateForTempSession( ILoggerFactory loggerFactory, - LogLevel minimumLogLevel, Stream inputStream, Stream outputStream, HostStartupInfo hostDetails) @@ -77,6 +95,10 @@ private PsesDebugServer( internal IServiceProvider ServiceProvider { get; } + /// + /// Start the debug server listening. + /// + /// A task that completes when the server is ready. public async Task StartAsync() { _jsonRpcServer = await JsonRpcServer.From(options => diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index aba06d55a..00c4b571e 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -17,6 +17,9 @@ namespace Microsoft.PowerShell.EditorServices.Server { + /// + /// Server runner class for handling LSP messages for Editor Services. + /// internal class PsesLanguageServer { internal ILoggerFactory LoggerFactory { get; private set; } @@ -29,21 +32,31 @@ internal class PsesLanguageServer private readonly HostStartupInfo _hostDetails; private readonly TaskCompletionSource _serverStart; + /// + /// Create a new language server instance. + /// + /// Factory to create loggers with. + /// Protocol transport input stream. + /// Protocol transport output stream. + /// Host configuration to instantiate the server and services with. public PsesLanguageServer( ILoggerFactory factory, - LogLevel minimumLogLevel, Stream inputStream, Stream outputStream, HostStartupInfo hostDetails) { LoggerFactory = factory; - _minimumLogLevel = minimumLogLevel; + _minimumLogLevel = (LogLevel)hostDetails.LogLevel; _inputStream = inputStream; _outputStream = outputStream; _hostDetails = hostDetails; _serverStart = new TaskCompletionSource(); } + /// + /// Start the server listening for input. + /// + /// A task that completes when the server is ready and listening. public async Task StartAsync() { LanguageServer = await OmniSharp.Extensions.LanguageServer.Server.LanguageServer.From(options => @@ -103,8 +116,14 @@ await serviceProvider.GetService().SetWorkingDirectory } }); }); + + _serverStart.SetResult(true); } + /// + /// Get a task that completes when the server is shut down. + /// + /// A task that completes when the server is shut down. public async Task WaitForShutdown() { await _serverStart.Task; From dbd3081466262f1dfc89ec0a793a42739eac42bb Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 26 Nov 2019 17:02:28 -0800 Subject: [PATCH 11/62] Plumb host logging into PSES --- .../Hosting/EditorServicesServerFactory.cs | 7 +++- .../Logging/HostLoggerAdapter.cs | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index 141aa39f6..af5531f66 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Server; +using PowerShellEditorServices.Logging; using Serilog; +using System; using System.Collections.Generic; using System.IO; @@ -21,7 +23,7 @@ internal class EditorServicesServerFactory /// The path of the log file to use. /// The minimum log level to use. /// - public static EditorServicesServerFactory Create(string logPath, int minimumLogLevel) + public static EditorServicesServerFactory Create(string logPath, int minimumLogLevel, IObservable<(int logLevel, string message)> hostLogger) { Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() @@ -31,6 +33,9 @@ public static EditorServicesServerFactory Create(string logPath, int minimumLogL ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + // Hook up logging from the host so that its recorded in the log file + hostLogger.Subscribe(new HostLoggerAdapter(loggerFactory)); + return new EditorServicesServerFactory(loggerFactory, (LogLevel)minimumLogLevel); } diff --git a/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs b/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs new file mode 100644 index 000000000..318b6dac2 --- /dev/null +++ b/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Text; + +namespace PowerShellEditorServices.Logging +{ + /// + /// Adapter class to allow logging events sent by the host to be recorded by PSES' logging infrastructure. + /// + internal class HostLoggerAdapter : IObserver<(int logLevel, string message)> + { + private readonly ILogger _logger; + + /// + /// Create a new host logger adapter. + /// + /// Factory to create logger instances with. + public HostLoggerAdapter(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger("HostLogs"); + } + + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + _logger.LogError(error, "Error in host logger"); + } + + public void OnNext((int logLevel, string message) value) + { + _logger.Log((LogLevel)value.logLevel, value.message); + } + } +} From 8fb83f9c51b74dbae0ce13f6f4fdb7a974bb8802 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 26 Nov 2019 17:07:38 -0800 Subject: [PATCH 12/62] Fix compile issues --- src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs | 2 +- src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs | 5 +++++ src/PowerShellEditorServices/Hosting/HostStartupInfo.cs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 9ccf9db24..26f7e86eb 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -45,7 +45,7 @@ private EditorServicesRunner(HostLogger logger, EditorServicesConfig config, ISe _logger = logger; _config = config; _sessionFileWriter = sessionFileWriter; - _serverFactory = EditorServicesServerFactory.Create(_config.LogPath, (int)_config.LogLevel); + _serverFactory = EditorServicesServerFactory.Create(_config.LogPath, (int)_config.LogLevel, logger); _alreadySubscribedDebug = false; } diff --git a/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs b/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs index e9dab1969..626b30d68 100644 --- a/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs +++ b/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs @@ -9,6 +9,11 @@ using System.IO.Pipes; using System.Runtime.InteropServices; +#if !CoreCLR +using System.Security.Principal; +using System.Security.AccessControl; +#endif + namespace Microsoft.PowerShell.EditorServices.Hosting { /// diff --git a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs index 43b392d3b..f794716de 100644 --- a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs +++ b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs @@ -8,7 +8,7 @@ namespace Microsoft.PowerShell.EditorServices.Hosting /// Contains details about the host as well as any other information /// needed by Editor Services at startup time. /// - internal class HostStartupInfo + public class HostStartupInfo { #region Constants From 7f041982781d9fd7f2d5ce988ed540d4089dd275 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Tue, 26 Nov 2019 18:06:54 -0800 Subject: [PATCH 13/62] First part of host logger handover --- .../EditorServicesLoader.cs | 17 ++++++++++---- .../EditorServicesRunner.cs | 23 ++++++++++++++++--- .../StartEditorServicesCommand.cs | 6 +++-- 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 61b2fea0b..71c9f9a6f 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -7,6 +7,7 @@ using System.IO; using System.Reflection; using System.Threading.Tasks; +using System.Collections.Generic; #if CoreCLR using System.Runtime.Loader; @@ -43,7 +44,8 @@ public sealed class EditorServicesLoader : IDisposable public static EditorServicesLoader Create( HostLogger logger, EditorServicesConfig hostConfig, - ISessionFileWriter sessionFileWriter) + ISessionFileWriter sessionFileWriter, + IReadOnlyCollection loggersToUnsubscribe = null) { #if CoreCLR // In .NET Core, we add an event here to redirect dependency loading to the new AssemblyLoadContext we load PSES' dependencies into @@ -55,7 +57,7 @@ public static EditorServicesLoader Create( AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_OnAssemblyResolve; #endif - return new EditorServicesLoader(logger, hostConfig, sessionFileWriter); + return new EditorServicesLoader(logger, hostConfig, sessionFileWriter, loggersToUnsubscribe); } private readonly EditorServicesConfig _hostConfig; @@ -64,11 +66,18 @@ public static EditorServicesLoader Create( private readonly HostLogger _logger; - private EditorServicesLoader(HostLogger logger, EditorServicesConfig hostConfig, ISessionFileWriter sessionFileWriter) + private readonly IReadOnlyCollection _loggersToUnsubscribe; + + private EditorServicesLoader( + HostLogger logger, + EditorServicesConfig hostConfig, + ISessionFileWriter sessionFileWriter, + IReadOnlyCollection loggersToUnsubscribe) { _logger = logger; _hostConfig = hostConfig; _sessionFileWriter = sessionFileWriter; + _loggersToUnsubscribe = loggersToUnsubscribe; } /// @@ -83,7 +92,7 @@ public async Task LoadAndRunEditorServicesAsync() EditorServicesLoading.LoadEditorServicesForHost(); _logger.Log(PsesLogLevel.Verbose, "Starting EditorServices"); - using (var editorServicesRunner = EditorServicesRunner.Create(_logger, _hostConfig, _sessionFileWriter)) + using (var editorServicesRunner = EditorServicesRunner.Create(_logger, _hostConfig, _sessionFileWriter, _loggersToUnsubscribe)) { // The trigger method for Editor Services // We will wait here until Editor Services shuts down diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 26f7e86eb..cb1c377d9 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -5,6 +5,7 @@ using Microsoft.PowerShell.EditorServices.Server; using System; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Threading.Tasks; @@ -25,9 +26,13 @@ internal class EditorServicesRunner : IDisposable /// The startup configuration to use. /// The session file writer to use. /// - public static EditorServicesRunner Create(HostLogger logger, EditorServicesConfig config, ISessionFileWriter sessionFileWriter) + public static EditorServicesRunner Create( + HostLogger logger, + EditorServicesConfig config, + ISessionFileWriter sessionFileWriter, + IReadOnlyCollection loggersToUnsubscribe) { - return new EditorServicesRunner(logger, config, sessionFileWriter); + return new EditorServicesRunner(logger, config, sessionFileWriter, loggersToUnsubscribe); } private readonly HostLogger _logger; @@ -38,15 +43,22 @@ public static EditorServicesRunner Create(HostLogger logger, EditorServicesConfi private readonly EditorServicesServerFactory _serverFactory; + private readonly IReadOnlyCollection _loggersToUnsubscribe; + private bool _alreadySubscribedDebug; - private EditorServicesRunner(HostLogger logger, EditorServicesConfig config, ISessionFileWriter sessionFileWriter) + private EditorServicesRunner( + HostLogger logger, + EditorServicesConfig config, + ISessionFileWriter sessionFileWriter, + IReadOnlyCollection loggersToUnsubscribe) { _logger = logger; _config = config; _sessionFileWriter = sessionFileWriter; _serverFactory = EditorServicesServerFactory.Create(_config.LogPath, (int)_config.LogLevel, logger); _alreadySubscribedDebug = false; + _loggersToUnsubscribe = loggersToUnsubscribe; } /// @@ -115,6 +127,11 @@ private async Task CreateEditorServicesAndRunUntilShutdown() StartDebugServer(debugServerCreation); } + foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) + { + loggerToUnsubscribe.Dispose(); + } + await languageServer.WaitForShutdown().ConfigureAwait(false); } diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 96585befa..d8e4319b4 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -19,6 +19,8 @@ public sealed class StartEditorServicesCommand : PSCmdlet { private HostLogger _logger; + private IDisposable _hostLoggerSubscription; + /// /// The name of the EditorServices host to report /// @@ -171,7 +173,7 @@ protected override void BeginProcessing() // Set up logging now for use throughout startup _logger = new HostLogger(LogLevel); - _logger.Subscribe(new PSHostLogger(Host.UI)); + _hostLoggerSubscription = _logger.Subscribe(new PSHostLogger(Host.UI)); _logger.Log(PsesLogLevel.Diagnostic, "Logger created"); } @@ -191,7 +193,7 @@ protected override void EndProcessing() var sessionFileWriter = new SessionFileWriter(_logger, SessionDetailsPath); _logger.Log(PsesLogLevel.Diagnostic, "Session file writer created"); - using (var psesLoader = EditorServicesLoader.Create(_logger, editorServicesConfig, sessionFileWriter)) + using (var psesLoader = EditorServicesLoader.Create(_logger, editorServicesConfig, sessionFileWriter, loggersToUnsubscribe: new[] { _hostLoggerSubscription })) { _logger.Log(PsesLogLevel.Verbose, "Loading EditorServices"); psesLoader.LoadAndRunEditorServicesAsync().Wait(); From 0580aea1e0fd6f8a242ee240e2ac649b19fd555c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 26 Nov 2019 19:51:36 -0800 Subject: [PATCH 14/62] Complete host logging deregistration --- .../EditorServicesHost.cs | 43 +++++++++++++++++++ .../PsesLogLevel.cs | 15 +++++++ .../EditorServicesRunner.cs | 16 ++++--- .../HostLogger.cs | 29 +++++++++---- 4 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 src/PowerShellEditorServices.Host/EditorServicesHost.cs create mode 100644 src/PowerShellEditorServices.Host/PsesLogLevel.cs diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs new file mode 100644 index 000000000..602d13c80 --- /dev/null +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -0,0 +1,43 @@ +using PowerShellEditorServices.Host; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.PowerShell.EditorServices.Host +{ + public enum ConsoleReplKind + { + None = 0, + PSReadLine, + LegacyReadLine, + } + + public abstract class EditorServicesConfiguration + { + public string HostName { get; set; } + + public string HostProfileId { get; set; } + + public string HostVersion { get; set; } + + public ConsoleReplKind ConsoleRepl { get; set; } + + public string BundledModulesPath { get; set; } + + public IReadOnlyList AdditionalModules { get; set; } + + public IReadOnlyList FeatureFlags { get; set; } + + public string LogPath { get; set; } + + public PsesLogLevel LogLevel { get; set; } + + public string SessionDetailsPath { get; set; } + } + + public class EditorServicesHost + { + } +} diff --git a/src/PowerShellEditorServices.Host/PsesLogLevel.cs b/src/PowerShellEditorServices.Host/PsesLogLevel.cs new file mode 100644 index 000000000..795f59e3b --- /dev/null +++ b/src/PowerShellEditorServices.Host/PsesLogLevel.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace PowerShellEditorServices.Host +{ + public enum PsesLogLevel + { + Diagnostic, + Verbose, + Normal, + Warning, + Error, + } +} diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index cb1c377d9..6914d9ea9 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -112,6 +112,17 @@ private async Task CreateEditorServicesAndRunUntilShutdown() // - Possibly start the debug server // - Wait for the LSP server to finish + _logger.Log(PsesLogLevel.Verbose, "Kicking off server, deregistering host logger and awaiting server shutdown"); + + // Unsubscribe the host logger here so that the integrated console is not polluted with input after the first prompt + foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) + { + loggerToUnsubscribe.Dispose(); + } + + // Write the integrated console banner + _config.PSHost.UI.WriteLine("\n=== PowerShell Integrated Console ==="); + PsesLanguageServer languageServer = await CreateLanguageServer(hostStartupInfo).ConfigureAwait(false); Task debugServerCreation = null; @@ -127,11 +138,6 @@ private async Task CreateEditorServicesAndRunUntilShutdown() StartDebugServer(debugServerCreation); } - foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) - { - loggerToUnsubscribe.Dispose(); - } - await languageServer.WaitForShutdown().ConfigureAwait(false); } diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index e0e514515..05bb608a7 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -4,6 +4,7 @@ // using System; +using System.Collections; using System.Collections.Concurrent; using System.Management.Automation.Host; using System.Runtime.CompilerServices; @@ -59,14 +60,24 @@ public void OnNext((PsesLogLevel logLevel, string message) value) } /// - /// Empty implementation for subscribers to unsubscribe from logging output. - /// Since this logger will stop being called soon after startup, - /// we don't really need to implement unsubscription logic (which can be a bit complicated). + /// Simple unsubscriber that allows subscribers to remove themselves from the observer list later. /// - private struct Unsubscriber : IDisposable + private class Unsubscriber : IDisposable { + private readonly ConcurrentDictionary, bool> _subscribedObservers; + + private readonly IObserver<(PsesLogLevel, string)> _thisSubscriber; + + + public Unsubscriber(ConcurrentDictionary, bool> subscribedObservers, IObserver<(PsesLogLevel, string)> thisSubscriber) + { + _subscribedObservers = subscribedObservers; + _thisSubscriber = thisSubscriber; + } + public void Dispose() { + _subscribedObservers.TryRemove(_thisSubscriber, out bool _); } } @@ -74,7 +85,7 @@ public void Dispose() private readonly ConcurrentQueue<(PsesLogLevel logLevel, string message)> _logMessages; - private readonly ConcurrentBag> _observers; + private readonly ConcurrentDictionary, bool> _observers; /// /// Construct a new logger in the host. @@ -84,7 +95,7 @@ public HostLogger(PsesLogLevel minimumLogLevel) { _minimumLogLevel = minimumLogLevel; _logMessages = new ConcurrentQueue<(PsesLogLevel logLevel, string message)>(); - _observers = new ConcurrentBag>(); + _observers = new ConcurrentDictionary, bool>(); } /// @@ -99,7 +110,7 @@ public IDisposable Subscribe(IObserver<(PsesLogLevel logLevel, string message)> throw new ArgumentNullException(nameof(observer)); } - _observers.Add(observer); + _observers[observer] = true; // Catch up a late subscriber to messages already logged foreach ((PsesLogLevel logLevel, string message) entry in _logMessages) @@ -107,7 +118,7 @@ public IDisposable Subscribe(IObserver<(PsesLogLevel logLevel, string message)> observer.OnNext(entry); } - return new Unsubscriber(); + return new Unsubscriber(_observers, observer); } /// @@ -142,7 +153,7 @@ public void Log(PsesLogLevel logLevel, string message) _logMessages.Enqueue((logLevel, message)); // Send this log to all observers - foreach (IObserver<(PsesLogLevel logLevel, string message)> observer in _observers) + foreach (IObserver<(PsesLogLevel logLevel, string message)> observer in _observers.Keys) { observer.OnNext((logLevel, message)); } From b073e53686fd22cd797d5f646c48ec1683873235 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 08:04:51 -0800 Subject: [PATCH 15/62] Add more logging on startup --- .../EditorServicesLoader.cs | 98 ++++++++++++++++++- .../EditorServicesRunner.cs | 6 +- 2 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 71c9f9a6f..02e6c71f7 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -8,6 +8,8 @@ using System.Reflection; using System.Threading.Tasks; using System.Collections.Generic; +using SMA = System.Management.Automation; +using System.Runtime.InteropServices; #if CoreCLR using System.Runtime.Loader; @@ -87,6 +89,12 @@ private EditorServicesLoader( /// public async Task LoadAndRunEditorServicesAsync() { + // Add the bundled modules to the PSModulePath + UpdatePSModulePath(); + + // Log important host information here + LogHostInformation(); + // Method with no implementation that forces the PSES assembly to load, triggering an AssemblyResolve event _logger.Log(PsesLogLevel.Verbose, "Loading PSES assemblies"); EditorServicesLoading.LoadEditorServicesForHost(); @@ -102,7 +110,95 @@ public async Task LoadAndRunEditorServicesAsync() public void Dispose() { - // TODO: Deregister assembly event +#if CoreCLR + AssemblyLoadContext.Default.Resolving -= DefaultLoadContext_OnAssemblyResolve; +#else + AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_OnAssemblyResolve; +#endif + } + + private void UpdatePSModulePath() + { + if (string.IsNullOrEmpty(_hostConfig.BundledModulePath)) + { + _logger.Log(PsesLogLevel.Diagnostic, "BundledModulePath not set, skipping"); + return; + } + + string psModulePath = Environment.GetEnvironmentVariable("PSModulePath").TrimEnd(Path.PathSeparator); + psModulePath = $"{psModulePath}{Path.PathSeparator}{_hostConfig.BundledModulePath}"; + Environment.SetEnvironmentVariable("PSModulePath", psModulePath); + + _logger.Log(PsesLogLevel.Verbose, $"Updated PSModulePath to: '{psModulePath}'"); + } + + private void LogHostInformation() + { + _logger.Log(PsesLogLevel.Verbose, $@" +== Host Startup Configuration Details == + - Host name: {_hostConfig.HostInfo.Name} + - Host version: {_hostConfig.HostInfo.Version} + - Host profile ID: {_hostConfig.HostInfo.ProfileId} + - PowerShell host type: {_hostConfig.PSHost.GetType()} + + - REPL setting: {_hostConfig.ConsoleRepl} + - Session details path: {_hostConfig.SessionDetailsPath} + - Bundled modules path: {_hostConfig.BundledModulePath} + - Additional modules: {_hostConfig.AdditionalModules} + - Feature flags: {_hostConfig.FeatureFlags} + + - Log path: {_hostConfig.LogPath} + - Minimum log level: {_hostConfig.LogLevel} + + - Configured current user profile path: {_hostConfig.ProfilePaths?.CurrentUserProfilePath ?? ""} + - Configured all users profile path: {_hostConfig.ProfilePaths?.AllUsersProfilePath ?? ""}"); + + _logger.Log(PsesLogLevel.Verbose, $@" +== Console Details == + - Console input encoding: {Console.InputEncoding.EncodingName} + - Console output encoding: {Console.OutputEncoding.EncodingName} + - PowerShell output encoding: {GetPSOutputEncoding()} +"); + + LogOperatingSystemDetails(); + } + + private string GetPSOutputEncoding() + { + using (var pwsh = SMA.PowerShell.Create()) + { + return pwsh.AddScript("$OutputEncoding.EncodingName").Invoke()[0]; + } + } + + private void LogOperatingSystemDetails() + { + _logger.Log(PsesLogLevel.Verbose, $@" +== Environment Details == + - OS description: {RuntimeInformation.OSDescription} + - OS architecture: {GetOSArchitecture()} + - Process bitness: {(Environment.Is64BitProcess ? "64" : "32")} +"); + } + + private string GetOSArchitecture() + { +#if CoreCLR + if (Environment.OSVersion.Platform != PlatformID.Win32NT) + { + return RuntimeInformation.OSArchitecture.ToString(); + } +#endif + + // If on win7 (version 6.1.x), avoid System.Runtime.InteropServices.RuntimeInformation + if (Environment.OSVersion.Version < new Version(6, 2)) + { + return Environment.Is64BitProcess + ? "X64" + : "X86"; + } + + return RuntimeInformation.OSArchitecture.ToString(); } #if CoreCLR diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 6914d9ea9..3d6794c4f 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -112,9 +112,8 @@ private async Task CreateEditorServicesAndRunUntilShutdown() // - Possibly start the debug server // - Wait for the LSP server to finish - _logger.Log(PsesLogLevel.Verbose, "Kicking off server, deregistering host logger and awaiting server shutdown"); - // Unsubscribe the host logger here so that the integrated console is not polluted with input after the first prompt + _logger.Log(PsesLogLevel.Verbose, "Starting server, deregistering host logger and registering shutdown listener"); foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) { loggerToUnsubscribe.Dispose(); @@ -139,6 +138,9 @@ private async Task CreateEditorServicesAndRunUntilShutdown() } await languageServer.WaitForShutdown().ConfigureAwait(false); + + // Resubscribe host logger to log shutdown events to the console + _logger.Subscribe(new PSHostLogger(_config.PSHost.UI)); } private async Task RunTempDebugSession(HostStartupInfo hostDetails) From b2eebcc74ffacb01f21ba9e5ac47a6945ac970f1 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 08:08:53 -0800 Subject: [PATCH 16/62] Migrate startup script --- .../PowerShellEditorServices.psm1 | 238 ------------------ .../Start-EditorServices.ps1 | 2 + 2 files changed, 2 insertions(+), 238 deletions(-) delete mode 100644 module/PowerShellEditorServices/PowerShellEditorServices.psm1 diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 deleted file mode 100644 index 770e516f8..000000000 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ /dev/null @@ -1,238 +0,0 @@ -# -# Copyright (c) Microsoft. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for full license information. -# - -# Need to load pipe handling shim assemblies in Windows PowerShell and PSCore 6.0 because they don't have WCP -if ($PSEdition -eq 'Desktop') { - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.IO.Pipes.AccessControl.dll" - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.Security.AccessControl.dll" - Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Desktop/System.Security.Principal.Windows.dll" -} - -Microsoft.PowerShell.Utility\Add-Type -Path "$PSScriptRoot/bin/Microsoft.PowerShell.EditorServices.dll" - -function Start-EditorServicesHost { - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $HostName, - - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $HostProfileId, - - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $HostVersion, - - [Parameter(ParameterSetName="Stdio",Mandatory=$true)] - [switch] - $Stdio, - - [Parameter(ParameterSetName="NamedPipe",Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $LanguageServiceNamedPipe, - - [Parameter(ParameterSetName="NamedPipe")] - [string] - $DebugServiceNamedPipe, - - [Parameter(ParameterSetName="NamedPipeSimplex",Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $LanguageServiceInNamedPipe, - - [Parameter(ParameterSetName="NamedPipeSimplex",Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $LanguageServiceOutNamedPipe, - - [Parameter(ParameterSetName="NamedPipeSimplex")] - [string] - $DebugServiceInNamedPipe, - - [Parameter(ParameterSetName="NamedPipeSimplex")] - [string] - $DebugServiceOutNamedPipe, - - [ValidateNotNullOrEmpty()] - [string] - $BundledModulesPath, - - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - $LogPath, - - [ValidateSet("Normal", "Verbose", "Error", "Diagnostic")] - $LogLevel = "Normal", - - [switch] - $EnableConsoleRepl, - - [switch] - $UseLegacyReadLine, - - [switch] - $DebugServiceOnly, - - [string[]] - $AdditionalModules = @(), - - [string[]] - [ValidateNotNull()] - $FeatureFlags = @(), - - [switch] - $WaitForDebugger - ) - - $editorServicesHost = $null - $hostDetails = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.HostDetails @( - $HostName, - $HostProfileId, - (Microsoft.PowerShell.Utility\New-Object System.Version @($HostVersion))) - - $editorServicesHost = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.EditorServicesHost @( - $hostDetails, - $BundledModulesPath, - $EnableConsoleRepl.IsPresent, - $UseLegacyReadLine.IsPresent, - $WaitForDebugger.IsPresent, - $AdditionalModules, - $FeatureFlags, - $Host) - - # Build the profile paths using the root paths of the current $profile variable - $profilePaths = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.ProfilePaths @( - $hostDetails.ProfileId, - [System.IO.Path]::GetDirectoryName($profile.AllUsersAllHosts), - [System.IO.Path]::GetDirectoryName($profile.CurrentUserAllHosts)) - - $editorServicesHost.StartLogging($LogPath, $LogLevel); - - $languageServiceConfig = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportConfig - - $debugServiceConfig = - Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportConfig - - switch ($PSCmdlet.ParameterSetName) { - "Stdio" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::Stdio - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::Stdio - break - } - "NamedPipe" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe - $languageServiceConfig.InOutPipeName = "$LanguageServiceNamedPipe" - if ($DebugServiceNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe - $debugServiceConfig.InOutPipeName = "$DebugServiceNamedPipe" - } - break - } - "NamedPipeSimplex" { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe - $languageServiceConfig.InPipeName = $LanguageServiceInNamedPipe - $languageServiceConfig.OutPipeName = $LanguageServiceOutNamedPipe - if ($DebugServiceInNamedPipe -and $DebugServiceOutNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Hosting.EditorServiceTransportType]::NamedPipe - $debugServiceConfig.InPipeName = $DebugServiceInNamedPipe - $debugServiceConfig.OutPipeName = $DebugServiceOutNamedPipe - } - break - } - } - - if ($DebugServiceOnly.IsPresent) { - $editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $true); - } elseif($Stdio.IsPresent) { - $editorServicesHost.StartLanguageService($languageServiceConfig, $profilePaths); - } else { - $editorServicesHost.StartLanguageService($languageServiceConfig, $profilePaths); - $editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $false); - } - - return $editorServicesHost -} - -function Compress-LogDir { - [CmdletBinding(SupportsShouldProcess=$true)] - param ( - [Parameter(Mandatory=$true, Position=0, HelpMessage="Literal path to a log directory.")] - [ValidateNotNullOrEmpty()] - [string] - $Path - ) - - begin { - function LegacyZipFolder($Path, $ZipPath) { - if (!(Microsoft.PowerShell.Management\Test-Path($ZipPath))) { - $zipMagicHeader = "PK" + [char]5 + [char]6 + ("$([char]0)" * 18) - Microsoft.PowerShell.Management\Set-Content -LiteralPath $ZipPath -Value $zipMagicHeader - (Microsoft.PowerShell.Management\Get-Item $ZipPath).IsReadOnly = $false - } - - $shellApplication = Microsoft.PowerShell.Utility\New-Object -ComObject Shell.Application - $zipPackage = $shellApplication.NameSpace($ZipPath) - - foreach ($file in (Microsoft.PowerShell.Management\Get-ChildItem -LiteralPath $Path)) { - $zipPackage.CopyHere($file.FullName) - Start-Sleep -MilliSeconds 500 - } - } - } - - end { - $zipPath = ((Microsoft.PowerShell.Management\Convert-Path $Path) -replace '(\\|/)$','') + ".zip" - - if (Get-Command Microsoft.PowerShell.Archive\Compress-Archive) { - if ($PSCmdlet.ShouldProcess($zipPath, "Create ZIP")) { - Microsoft.PowerShell.Archive\Compress-Archive -LiteralPath $Path -DestinationPath $zipPath -Force -CompressionLevel Optimal - $zipPath - } - } - else { - if ($PSCmdlet.ShouldProcess($zipPath, "Create Legacy ZIP")) { - LegacyZipFolder $Path $zipPath - $zipPath - } - } - } -} - -function Get-PowerShellEditorServicesVersion { - $nl = [System.Environment]::NewLine - - $versionInfo = "PSES module version: $($MyInvocation.MyCommand.Module.Version)$nl" - - $versionInfo += "PSVersion: $($PSVersionTable.PSVersion)$nl" - if ($PSVersionTable.PSEdition) { - $versionInfo += "PSEdition: $($PSVersionTable.PSEdition)$nl" - } - $versionInfo += "PSBuildVersion: $($PSVersionTable.BuildVersion)$nl" - $versionInfo += "CLRVersion: $($PSVersionTable.CLRVersion)$nl" - - $versionInfo += "Operating system: " - if ($IsLinux) { - $versionInfo += "Linux $(lsb_release -d -s)$nl" - } - elseif ($IsOSX) { - $versionInfo += "macOS $(lsb_release -d -s)$nl" - } - else { - $osInfo = CimCmdlets\Get-CimInstance Win32_OperatingSystem - $versionInfo += "Windows $($osInfo.OSArchitecture) $($osInfo.Version)$nl" - } - - $versionInfo -} diff --git a/module/PowerShellEditorServices/Start-EditorServices.ps1 b/module/PowerShellEditorServices/Start-EditorServices.ps1 index 0b1499073..cbc0fc92f 100644 --- a/module/PowerShellEditorServices/Start-EditorServices.ps1 +++ b/module/PowerShellEditorServices/Start-EditorServices.ps1 @@ -108,3 +108,5 @@ param( $DebugServiceOutPipeName = $null ) +Import-Module -Name "$PSScriptRoot/PowerShellEditorServices.psd1" +Start-EditorServices @PSBoundParameters From fd6d25e3c3c398b2e1b51486d46e496ed1e75ad5 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 08:40:42 -0800 Subject: [PATCH 17/62] Fix Windows PowerShell startup --- .../SessionFileWriter.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs index 6756da13a..7ccc5c416 100644 --- a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs +++ b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs @@ -4,7 +4,9 @@ // using System.Collections.Generic; +using System.IO; using System.Management.Automation; +using System.Text; using SMA = System.Management.Automation; namespace Microsoft.PowerShell.EditorServices.Hosting @@ -35,7 +37,10 @@ public interface ISessionFileWriter /// public class SessionFileWriter : ISessionFileWriter { - private HostLogger _logger; + // Use BOM-less UTF-8 for session file + private static readonly Encoding s_sessionFileEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + private readonly HostLogger _logger; private readonly string _sessionFilePath; @@ -128,15 +133,12 @@ private void WriteSessionObject(Dictionary sessionObject) using (var pwsh = SMA.PowerShell.Create(RunspaceMode.NewRunspace)) { content = pwsh.AddCommand("ConvertTo-Json") - .AddParameter("InputObject", sessionObject) - .AddParameter("Depth", 10) - .AddParameter("Compress") - .AddCommand("Set-Content") - .AddParameter("Path", _sessionFilePath) - .AddParameter("Encoding", "utf8") - .AddParameter("Force") - .AddParameter("PassThru") + .AddParameter("InputObject", sessionObject) + .AddParameter("Depth", 10) + .AddParameter("Compress") .Invoke()[0]; + + File.WriteAllText(_sessionFilePath, content, s_sessionFileEncoding); } _logger.Log(PsesLogLevel.Verbose, $"Session file written to {_sessionFilePath} with content:\n{content}"); From 14fc131f01528853a2bcec49f1f3e39d0d8a1d2c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 09:39:59 -0800 Subject: [PATCH 18/62] Add event handler logging --- .../EditorServicesLoader.cs | 78 +++++++++---------- .../PsesLoadContext.cs | 6 +- 2 files changed, 43 insertions(+), 41 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 02e6c71f7..13b45ebd0 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -32,10 +32,6 @@ public sealed class EditorServicesLoader : IDisposable "..", "Common")); -#if CoreCLR - private static readonly AssemblyLoadContext s_coreAsmLoadContext = new PsesLoadContext(s_psesDependencyDirPath); -#endif - /// /// Create a new Editor Services loader. /// @@ -51,12 +47,46 @@ public static EditorServicesLoader Create( { #if CoreCLR // In .NET Core, we add an event here to redirect dependency loading to the new AssemblyLoadContext we load PSES' dependencies into + logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for new AssemblyLoadContext dependency loading"); - AssemblyLoadContext.Default.Resolving += DefaultLoadContext_OnAssemblyResolve; + + var psesLoadContext = new PsesLoadContext(logger, s_psesDependencyDirPath); + + AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext defaultLoadContext, AssemblyName asmName) => + { + logger.Log(PsesLogLevel.Diagnostic, $"Assembly resolve event fired for {asmName}"); + + // We only want the Editor Services DLL; the new ALC will lazily load its dependencies automatically + if (!string.Equals(asmName.Name, "Microsoft.PowerShell.EditorServices", StringComparison.Ordinal)) + { + return null; + } + + string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); + + logger.Log(PsesLogLevel.Verbose, "Loading PSES DLL using new assembly load context"); + + return psesLoadContext.LoadFromAssemblyPath(asmPath); + }; #else // In .NET Framework we add an event here to redirect dependency loading in the current AppDomain for PSES' dependencies logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for dependency loading"); - AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_OnAssemblyResolve; + AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs args) => + { + logger.Log(PsesLogLevel.Diagnostic, $"Assembly resolve event fired for {args.Name}"); + + var asmName = new AssemblyName(args.Name); + + string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); + + if (!File.Exists(asmPath)) + { + return null; + } + + logger.Log(PsesLogLevel.Diagnostic, $"Loading {args.Name} from PSES dependency dir into LoadFrom context"); + return Assembly.LoadFrom(asmPath); + }; #endif return new EditorServicesLoader(logger, hostConfig, sessionFileWriter, loggersToUnsubscribe); @@ -110,11 +140,8 @@ public async Task LoadAndRunEditorServicesAsync() public void Dispose() { -#if CoreCLR - AssemblyLoadContext.Default.Resolving -= DefaultLoadContext_OnAssemblyResolve; -#else - AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_OnAssemblyResolve; -#endif + // TODO: Remove assembly resolve events + // This is not high priority, since the PSES process shouldn't be reused } private void UpdatePSModulePath() @@ -200,34 +227,5 @@ private string GetOSArchitecture() return RuntimeInformation.OSArchitecture.ToString(); } - -#if CoreCLR - private static Assembly DefaultLoadContext_OnAssemblyResolve(AssemblyLoadContext defaultLoadContext, AssemblyName asmName) - { - // We only want the Editor Services DLL; the new ALC will lazily load its dependencies automatically - if (!string.Equals(asmName.Name, "Microsoft.PowerShell.EditorServices", StringComparison.Ordinal)) - { - return null; - } - - string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); - - return s_coreAsmLoadContext.LoadFromAssemblyPath(asmPath); - } -#endif - -#if !CoreCLR - private static Assembly CurrentDomain_OnAssemblyResolve(object sender, ResolveEventArgs args) - { - var asmName = new AssemblyName(args.Name); - - string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); - - return File.Exists(asmPath) - ? Assembly.LoadFrom(asmPath) - : null; - } -#endif - } } diff --git a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs index ccd5563ce..6afa4df5d 100644 --- a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs +++ b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs @@ -17,10 +17,13 @@ namespace Microsoft.PowerShell.EditorServices.Hosting /// internal class PsesLoadContext : AssemblyLoadContext { + private readonly HostLogger _logger; + private readonly string _dependencyDirPath; - public PsesLoadContext(string dependencyDirPath) + public PsesLoadContext(HostLogger logger, string dependencyDirPath) { + _logger = logger; _dependencyDirPath = dependencyDirPath; } @@ -30,6 +33,7 @@ protected override Assembly Load(AssemblyName assemblyName) if (File.Exists(asmPath)) { + _logger.Log(PsesLogLevel.Diagnostic, $"Loading assembly '{assemblyName}' in isolated load context"); return LoadFromAssemblyPath(asmPath); } From accfa5207ec3f94aa69eab718303f3ec91060227 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 11:25:06 -0800 Subject: [PATCH 19/62] Add warning for low .NET versions --- .../EditorServicesLoader.cs | 24 +++++++++++++++++++ .../Hosting/EditorServicesServerFactory.cs | 1 - 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 13b45ebd0..b71f21c42 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using SMA = System.Management.Automation; using System.Runtime.InteropServices; +using Microsoft.Win32; #if CoreCLR using System.Runtime.Loader; @@ -119,6 +120,10 @@ private EditorServicesLoader( /// public async Task LoadAndRunEditorServicesAsync() { +#if !CoreCLR + CheckNetFxVersion(); +#endif + // Add the bundled modules to the PSModulePath UpdatePSModulePath(); @@ -144,6 +149,25 @@ public void Dispose() // This is not high priority, since the PSES process shouldn't be reused } +#if !CoreCLR + private void CheckNetFxVersion() + { + using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Net Framework Setup\NDP\v4\Full")) + { + object netFxValue = key?.GetValue("Release"); + if (netFxValue == null || !(netFxValue is int netFxVersion)) + { + return; + } + + if (netFxVersion < Net461Version) + { + _logger.Log(PsesLogLevel.Warning, $".NET Framework version {netFxVersion} lower than .NET 4.6.1. This runtime is not supported and you may experience errors. Please update your .NET runtime version."); + } + } + } +#endif + private void UpdatePSModulePath() { if (string.IsNullOrEmpty(_hostConfig.BundledModulePath)) diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index af5531f66..92a26c64d 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -3,7 +3,6 @@ using PowerShellEditorServices.Logging; using Serilog; using System; -using System.Collections.Generic; using System.IO; namespace Microsoft.PowerShell.EditorServices.Hosting From 1cebbb682bb689af09c70ea09f7a7dc34a20e608 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 12:35:39 -0800 Subject: [PATCH 20/62] Fix VSCode and Command module imports --- PowerShellEditorServices.build.ps1 | 24 +++++++++++++------ .../StartEditorServicesCommand.cs | 17 ++++++++++++- .../PowerShellContextService.cs | 23 ++++++++---------- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index e6fabc72c..93feeccb3 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -262,13 +262,15 @@ task TestE2E { } task LayoutModule -After Build { - $psesOutputPath = "$PSScriptRoot/module/PowerShellEditorServices" + $modulesDir = "$PSScriptRoot/module" + $psesVSCodeBinOutputPath = "$modulesDir/PowerShellEditorServices.VSCode/bin" + $psesOutputPath = "$modulesDir/PowerShellEditorServices" $psesBinOutputPath = "$PSScriptRoot/module/PowerShellEditorServices/bin" $psesDepsPath = "$psesBinOutputPath/Common" $psesCoreHostPath = "$psesBinOutputPath/Core" $psesDeskHostPath = "$psesBinOutputPath/Desktop" - foreach ($dir in $psesDepsPath,$psesCoreHostPath,$psesDeskHostPath) + foreach ($dir in $psesDepsPath,$psesCoreHostPath,$psesDeskHostPath,$psesVSCodeBinOutputPath) { New-Item -Force -Path $dir -ItemType Directory } @@ -276,7 +278,7 @@ task LayoutModule -After Build { # Copy Third Party Notices.txt to module folder Copy-Item -Force -Path "$PSScriptRoot\Third Party Notices.txt" -Destination $psesOutputPath - $psesDlls = [System.Collections.Generic.HashSet[string]]::new() + $includedDlls = [System.Collections.Generic.HashSet[string]]::new() foreach ($psesComponent in Get-ChildItem $script:PsesOutput) { if ($psesComponent.Name -eq 'System.Management.Automation.dll') @@ -284,16 +286,16 @@ task LayoutModule -After Build { continue } - if ($psesComponent.Extension -eq '.dll') + if ($psesComponent.Extension) { - [void]$psesDlls.Add($psesComponent.Name) + [void]$includedDlls.Add($psesComponent.Name) Copy-Item -Path $psesComponent.FullName -Destination $psesDepsPath } } foreach ($hostComponent in Get-ChildItem $script:HostCoreOutput) { - if (-not $psesDlls.Contains($hostComponent.Name)) + if (-not $includedDlls.Contains($hostComponent.Name)) { Copy-Item -Path $hostComponent.FullName -Destination $psesCoreHostPath } @@ -303,12 +305,20 @@ task LayoutModule -After Build { { foreach ($hostComponent in Get-ChildItem $script:HostDeskOutput) { - if (-not $psesDlls.Contains($hostComponent.Name)) + if (-not $includedDlls.Contains($hostComponent.Name)) { Copy-Item -Path $hostComponent.FullName -Destination $psesDeskHostPath } } } + + foreach ($vscodeComponent in Get-ChildItem $script:VSCodeOutput) + { + if (-not $includedDlls.Contains($vscodeComponent.Name)) + { + Copy-Item -Path $vscodeComponent.FullName -Destination $psesVSCodeBinOutputPath + } + } } task RestorePsesModules -After Build { diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index d8e4319b4..83c3075ce 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -5,8 +5,10 @@ using Microsoft.PowerShell.Commands; using System; +using System.IO; using System.Linq; using System.Management.Automation; +using System.Reflection; using SMA = System.Management.Automation; namespace Microsoft.PowerShell.EditorServices.Hosting @@ -236,8 +238,21 @@ private void RemovePSReadLineForStartup() private EditorServicesConfig CreateConfigObject() { _logger.Log(PsesLogLevel.Diagnostic, "Creating host configuration"); + + string bundledModulesPath = BundledModulesPath; + if (!Path.IsPathRooted(bundledModulesPath)) + { + // For compatibility, the bundled modules path is relative to the PSES bin directory + // Ideally it should be one level up, the PSES module root + bundledModulesPath = Path.GetFullPath( + Path.Combine( + Assembly.GetExecutingAssembly().Location, + "..", + bundledModulesPath)); + } + var hostInfo = new HostInfo(HostName, HostProfileId, HostVersion); - var editorServicesConfig = new EditorServicesConfig(hostInfo, Host, SessionDetailsPath, BundledModulesPath, LogPath) + var editorServicesConfig = new EditorServicesConfig(hostInfo, Host, SessionDetailsPath, bundledModulesPath, LogPath) { FeatureFlags = FeatureFlags, LogLevel = LogLevel, diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index ddc43b150..0f4cee21c 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -34,6 +34,11 @@ namespace Microsoft.PowerShell.EditorServices.Services /// public class PowerShellContextService : IDisposable, IHostSupportsInteractiveSession { + private static readonly string s_commandsModulePath = Path.GetFullPath( + Path.Combine( + Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), + "../../Commands/PowerShellEditorServices.Commands.psd1")); + private static readonly Action s_runspaceApartmentStateSetter; static PowerShellContextService() @@ -202,10 +207,7 @@ public static PowerShellContextService Create( var profilePaths = new ProfilePaths(hostDetails.ProfileId, hostDetails.AllUsersProfilePath, hostDetails.CurrentUserProfilePath); powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); - powerShellContext.ImportCommandsModuleAsync( - Path.Combine( - Path.GetDirectoryName(typeof(PowerShellContextService).GetTypeInfo().Assembly.Location), - @"..\Commands")); + powerShellContext.ImportCommandsModuleAsync(); // TODO: This can be moved to the point after the $psEditor object // gets initialized when that is done earlier than LanguageServer.Initialize @@ -418,19 +420,14 @@ public void Initialize( /// Imports the PowerShellEditorServices.Commands module into /// the runspace. This method will be moved somewhere else soon. /// - /// /// - public Task ImportCommandsModuleAsync(string moduleBasePath) + public Task ImportCommandsModuleAsync() { - PSCommand importCommand = new PSCommand(); - importCommand + PSCommand importCommand = new PSCommand() .AddCommand("Import-Module") - .AddArgument( - Path.Combine( - moduleBasePath, - "PowerShellEditorServices.Commands.psd1")); + .AddArgument(s_commandsModulePath); - return this.ExecuteCommandAsync(importCommand, false, false); + return this.ExecuteCommandAsync(importCommand, sendOutputToHost: false, sendErrorToHost: false); } private static bool CheckIfRunspaceNeedsEventHandlers(RunspaceDetails runspaceDetails) From 640f97bdcf1844f232273493ea30d90836a6c34c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 13:14:04 -0800 Subject: [PATCH 21/62] Add constrained language mode check --- .../EditorServicesLoader.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index b71f21c42..276368217 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -124,6 +124,8 @@ public async Task LoadAndRunEditorServicesAsync() CheckNetFxVersion(); #endif + CheckLanguageMode(); + // Add the bundled modules to the PSModulePath UpdatePSModulePath(); @@ -168,6 +170,21 @@ private void CheckNetFxVersion() } #endif + /// + /// PSES currently does not work in Constrained Language Mode, because PSReadLine script invocations won't work in it. + /// Ideally we can find a better way so that PSES will work in CLM. + /// + private void CheckLanguageMode() + { + using (var pwsh = SMA.PowerShell.Create()) + { + if (pwsh.Runspace.SessionStateProxy.LanguageMode != SMA.PSLanguageMode.FullLanguage) + { + throw new InvalidOperationException("Cannot start PowerShell Editor Services in Constrained Language Mode"); + } + } + } + private void UpdatePSModulePath() { if (string.IsNullOrEmpty(_hostConfig.BundledModulePath)) From 9bd8789c45a8c6e6f3c550dc06b6e03629c9ac63 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 13:21:07 -0800 Subject: [PATCH 22/62] Add copyright headers --- .../Hosting/EditorServicesLoading.cs | 4 ++++ .../Hosting/EditorServicesServerFactory.cs | 7 ++++++- src/PowerShellEditorServices/Hosting/HostStartupInfo.cs | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs b/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs index 0578a6089..5669b084b 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesLoading.cs @@ -1,3 +1,7 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// namespace Microsoft.PowerShell.EditorServices.Hosting { diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index 92a26c64d..2a6f8fdf5 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -1,4 +1,9 @@ -using Microsoft.Extensions.Logging; +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Server; using PowerShellEditorServices.Logging; using Serilog; diff --git a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs index f794716de..c8d9b4f2c 100644 --- a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs +++ b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs @@ -1,3 +1,8 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + using System; using System.Collections.Generic; using System.Management.Automation.Host; From c18b5e7a942dea2e44828116d91abb9f2827b914 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 13:23:28 -0800 Subject: [PATCH 23/62] Remove extraneous host files --- .../EditorServicesHost.cs | 43 ------------------- .../PsesLogLevel.cs | 15 ------- 2 files changed, 58 deletions(-) delete mode 100644 src/PowerShellEditorServices.Host/EditorServicesHost.cs delete mode 100644 src/PowerShellEditorServices.Host/PsesLogLevel.cs diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs deleted file mode 100644 index 602d13c80..000000000 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ /dev/null @@ -1,43 +0,0 @@ -using PowerShellEditorServices.Host; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.PowerShell.EditorServices.Host -{ - public enum ConsoleReplKind - { - None = 0, - PSReadLine, - LegacyReadLine, - } - - public abstract class EditorServicesConfiguration - { - public string HostName { get; set; } - - public string HostProfileId { get; set; } - - public string HostVersion { get; set; } - - public ConsoleReplKind ConsoleRepl { get; set; } - - public string BundledModulesPath { get; set; } - - public IReadOnlyList AdditionalModules { get; set; } - - public IReadOnlyList FeatureFlags { get; set; } - - public string LogPath { get; set; } - - public PsesLogLevel LogLevel { get; set; } - - public string SessionDetailsPath { get; set; } - } - - public class EditorServicesHost - { - } -} diff --git a/src/PowerShellEditorServices.Host/PsesLogLevel.cs b/src/PowerShellEditorServices.Host/PsesLogLevel.cs deleted file mode 100644 index 795f59e3b..000000000 --- a/src/PowerShellEditorServices.Host/PsesLogLevel.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace PowerShellEditorServices.Host -{ - public enum PsesLogLevel - { - Diagnostic, - Verbose, - Normal, - Warning, - Error, - } -} From 72da2737423c2141436bf6f2f5ae39d53ea89902 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 16:25:41 -0800 Subject: [PATCH 24/62] Warn user about read key on crash --- .../StartEditorServicesCommand.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 83c3075ce..9f9b25ea7 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -206,6 +206,7 @@ protected override void EndProcessing() _logger.LogException("Exception encountered starting EditorServices", e); // Give the user a chance to read the message + Host.UI.WriteLine("\n== Press any key to close terminal =="); Console.ReadKey(); ThrowTerminatingError(new ErrorRecord(e, "PowerShellEditorServicesError", ErrorCategory.NotSpecified, this)); From 97bbae7b0e5e8f358807cec37e15a0b1fd076e44 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 17:03:40 -0800 Subject: [PATCH 25/62] Add configuration validation --- .../EditorServicesLoader.cs | 28 +++++++++++++++++++ .../EditorServicesRunner.cs | 2 -- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 276368217..340d37756 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -121,9 +121,11 @@ private EditorServicesLoader( public async Task LoadAndRunEditorServicesAsync() { #if !CoreCLR + // Make sure the .NET Framework version supports .NET Standard 2.0 CheckNetFxVersion(); #endif + // Ensure the language mode allows us to run CheckLanguageMode(); // Add the bundled modules to the PSModulePath @@ -132,6 +134,9 @@ public async Task LoadAndRunEditorServicesAsync() // Log important host information here LogHostInformation(); + // Check to see if the configuration we have is valid + ValidateConfiguration(); + // Method with no implementation that forces the PSES assembly to load, triggering an AssemblyResolve event _logger.Log(PsesLogLevel.Verbose, "Loading PSES assemblies"); EditorServicesLoading.LoadEditorServicesForHost(); @@ -268,5 +273,28 @@ private string GetOSArchitecture() return RuntimeInformation.OSArchitecture.ToString(); } + + private void ValidateConfiguration() + { + bool lspUsesStdio = _hostConfig.LanguageServiceTransport is StdioTransportConfig; + bool debugUsesStdio = _hostConfig.DebugServiceTransport is StdioTransportConfig; + + // Ensure LSP and Debug are not both Stdio + if (lspUsesStdio && debugUsesStdio) + { + throw new ArgumentException("LSP and Debug transports cannot both use Stdio"); + } + + if (_hostConfig.ConsoleRepl != ConsoleReplKind.None + && (lspUsesStdio || debugUsesStdio)) + { + throw new ArgumentException("Cannot use the REPL with a Stdio protocol transport"); + } + + if (_hostConfig.PSHost == null) + { + throw new ArgumentNullException(nameof(_hostConfig.PSHost)); + } + } } } diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 3d6794c4f..bb37b0c0c 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -92,8 +92,6 @@ private async Task CreateEditorServicesAndRunUntilShutdown() bool creatingDebugServer = _config.DebugServiceTransport != null; bool isTempDebugSession = creatingDebugServer && !creatingLanguageServer; - // TODO: Validate config here - // Set up information required to instantiate servers HostStartupInfo hostStartupInfo = CreateHostStartupInfo(); From d744818e052a618ad4060b7ecd1912ea43f52260 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Wed, 27 Nov 2019 17:46:52 -0800 Subject: [PATCH 26/62] Add more logging --- src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 340d37756..d27f463c9 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -159,6 +159,7 @@ public void Dispose() #if !CoreCLR private void CheckNetFxVersion() { + _logger.Log(PsesLogLevel.Diagnostic, "Checking that .NET Framework version is at least 4.6.1"); using (RegistryKey key = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Net Framework Setup\NDP\v4\Full")) { object netFxValue = key?.GetValue("Release"); @@ -181,6 +182,7 @@ private void CheckNetFxVersion() /// private void CheckLanguageMode() { + _logger.Log(PsesLogLevel.Diagnostic, "Checking that PSES is running in FullLanguage mode"); using (var pwsh = SMA.PowerShell.Create()) { if (pwsh.Runspace.SessionStateProxy.LanguageMode != SMA.PSLanguageMode.FullLanguage) @@ -207,6 +209,7 @@ private void UpdatePSModulePath() private void LogHostInformation() { + _logger.Log(PsesLogLevel.Diagnostic, "Logging host information"); _logger.Log(PsesLogLevel.Verbose, $@" == Host Startup Configuration Details == - Host name: {_hostConfig.HostInfo.Name} @@ -276,6 +279,8 @@ private string GetOSArchitecture() private void ValidateConfiguration() { + _logger.Log(PsesLogLevel.Diagnostic, "Validating configuration"); + bool lspUsesStdio = _hostConfig.LanguageServiceTransport is StdioTransportConfig; bool debugUsesStdio = _hostConfig.DebugServiceTransport is StdioTransportConfig; From 8c3414754a61857d71a1c34dc67febf7d74234d9 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Mon, 2 Dec 2019 11:26:52 -0800 Subject: [PATCH 27/62] Fix profile path issue --- .../EditorServicesConfig.cs | 23 ++-- .../EditorServicesLoader.cs | 8 +- .../EditorServicesRunner.cs | 43 +++---- .../StartEditorServicesCommand.cs | 39 ++++++- .../Hosting/HostStartupInfo.cs | 47 +++++--- .../PowerShellContextService.cs | 35 ++++-- .../PowerShellContext/Session/ProfilePaths.cs | 108 ------------------ 7 files changed, 120 insertions(+), 183 deletions(-) delete mode 100644 src/PowerShellEditorServices/Services/PowerShellContext/Session/ProfilePaths.cs diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs index ad1ca2ceb..414baafb5 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs @@ -109,33 +109,26 @@ public EditorServicesConfig( /// PowerShell profile locations for Editor Services to use for its profiles. /// If none are provided, these will be generated from the hosting PowerShell's profile paths. /// - public ProfilePathConfig ProfilePaths { get; set; } = null; + public ProfilePathConfig ProfilePaths { get; set; } } /// /// Configuration for Editor Services' PowerShell profile paths. /// - public class ProfilePathConfig + public struct ProfilePathConfig { - /// - /// Create a new profile path configuration. - /// - /// The shared profile path. - /// The single user profile path. - public ProfilePathConfig(string allUsersPath, string currentUserPath) - { - AllUsersProfilePath = allUsersPath; - CurrentUserProfilePath = currentUserPath; - } - /// /// The path to the profile shared by all users. /// - public string AllUsersProfilePath { get; } + public string AllUsersAllHosts { get; set; } + + public string AllUsersCurrentHost { get; set; } /// /// The path to the profile specific to the current user. /// - public string CurrentUserProfilePath { get; } + public string CurrentUserAllHosts { get; set; } + + public string CurrentUserCurrentHost { get; set; } } } diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index d27f463c9..569511f80 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -226,8 +226,12 @@ private void LogHostInformation() - Log path: {_hostConfig.LogPath} - Minimum log level: {_hostConfig.LogLevel} - - Configured current user profile path: {_hostConfig.ProfilePaths?.CurrentUserProfilePath ?? ""} - - Configured all users profile path: {_hostConfig.ProfilePaths?.AllUsersProfilePath ?? ""}"); + - Profile paths: + + AllUsersAllHosts: {_hostConfig.ProfilePaths.AllUsersAllHosts ?? ""} + + AllUsersCurrentHost: {_hostConfig.ProfilePaths.AllUsersCurrentHost ?? ""} + + CurrentUserAllHosts: {_hostConfig.ProfilePaths.CurrentUserAllHosts ?? ""} + + CurrentUserCurrentHost: {_hostConfig.ProfilePaths.CurrentUserCurrentHost ?? ""} +"); _logger.Log(PsesLogLevel.Verbose, $@" == Console Details == diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index bb37b0c0c..deb7c3f90 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -215,15 +215,25 @@ private HostStartupInfo CreateHostStartupInfo() { _logger.Log(PsesLogLevel.Diagnostic, "Creating startup info object"); - (string allUsersProfilePath, string currentUserProfilePath) = GetProfilePaths(_config.ProfilePaths?.AllUsersProfilePath, _config.ProfilePaths?.CurrentUserProfilePath); + ProfilePathInfo profilePaths = null; + if (_config.ProfilePaths.AllUsersAllHosts != null + || _config.ProfilePaths.AllUsersCurrentHost != null + || _config.ProfilePaths.CurrentUserAllHosts != null + || _config.ProfilePaths.CurrentUserCurrentHost != null) + { + profilePaths = new ProfilePathInfo( + _config.ProfilePaths.CurrentUserAllHosts, + _config.ProfilePaths.CurrentUserCurrentHost, + _config.ProfilePaths.AllUsersAllHosts, + _config.ProfilePaths.AllUsersCurrentHost); + } return new HostStartupInfo( _config.HostInfo.Name, _config.HostInfo.ProfileId, _config.HostInfo.Version, _config.PSHost, - allUsersProfilePath, - currentUserProfilePath, + profilePaths, _config.FeatureFlags, _config.AdditionalModules, _config.LogPath, @@ -232,33 +242,6 @@ private HostStartupInfo CreateHostStartupInfo() usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine); } - private (string allUsersPath, string currentUserPath) GetProfilePaths(string allUsersPath, string currentUserPath) - { - _logger.Log(PsesLogLevel.Diagnostic, "Configuring profile paths"); - - if (allUsersPath == null || currentUserPath == null) - { - using (var pwsh = SMA.PowerShell.Create()) - { - _logger.Log(PsesLogLevel.Diagnostic, "Querying PowerShell for profile paths"); - Collection profiles = pwsh.AddScript("$profile.AllUsersAllHosts,$profile.CurrentUserAllHosts") - .Invoke(); - - if (allUsersPath == null) - { - allUsersPath = profiles[0]; - } - - if (currentUserPath == null) - { - currentUserPath = profiles[1]; - } - } - } - - return (allUsersPath, currentUserPath); - } - private void DebugServer_OnSessionEnded(object sender, EventArgs args) { _logger.Log(PsesLogLevel.Verbose, "Debug session ended. Restarting debug service"); diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 9f9b25ea7..8a1b2a47b 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -115,7 +115,7 @@ public sealed class StartEditorServicesCommand : PSCmdlet /// The minimum log level that should be emitted. /// [Parameter] - public PsesLogLevel LogLevel { get; set; } + public PsesLogLevel LogLevel { get; set; } = PsesLogLevel.Normal; /// /// Paths to additional PowerShell modules to be imported at startup. @@ -252,6 +252,8 @@ private EditorServicesConfig CreateConfigObject() bundledModulesPath)); } + var profile = (PSObject)GetVariableValue("profile"); + var hostInfo = new HostInfo(HostName, HostProfileId, HostVersion); var editorServicesConfig = new EditorServicesConfig(hostInfo, Host, SessionDetailsPath, bundledModulesPath, LogPath) { @@ -261,11 +263,34 @@ private EditorServicesConfig CreateConfigObject() AdditionalModules = AdditionalModules, LanguageServiceTransport = GetLanguageServiceTransport(), DebugServiceTransport = GetDebugServiceTransport(), + ProfilePaths = new ProfilePathConfig + { + AllUsersAllHosts = GetProfilePathFromProfileObject(profile, ProfileUserKind.AllUsers, ProfileHostKind.AllHosts), + AllUsersCurrentHost = GetProfilePathFromProfileObject(profile, ProfileUserKind.AllUsers, ProfileHostKind.CurrentHost), + CurrentUserAllHosts = GetProfilePathFromProfileObject(profile, ProfileUserKind.CurrentUser, ProfileHostKind.AllHosts), + CurrentUserCurrentHost = GetProfilePathFromProfileObject(profile, ProfileUserKind.CurrentUser, ProfileHostKind.CurrentHost), + }, }; return editorServicesConfig; } + private string GetProfilePathFromProfileObject(PSObject profileObject, ProfileUserKind userKind, ProfileHostKind hostKind) + { + string profilePathName = $"{userKind}{hostKind}"; + + string pwshProfilePath = (string)profileObject.Properties[profilePathName].Value; + + if (hostKind == ProfileHostKind.AllHosts) + { + return pwshProfilePath; + } + + return Path.Combine( + Path.GetDirectoryName(pwshProfilePath), + $"{HostProfileId}_profile.ps1"); + } + private ConsoleReplKind GetReplKind() { _logger.Log(PsesLogLevel.Diagnostic, "Determining REPL kind"); @@ -341,5 +366,17 @@ private ITransportConfig GetDebugServiceTransport() return DuplexNamedPipeTransportConfig.Create(DebugServicePipeName); } + + private enum ProfileHostKind + { + AllHosts, + CurrentHost, + } + + private enum ProfileUserKind + { + AllUsers, + CurrentUser, + } } } diff --git a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs index c8d9b4f2c..f4260e8a5 100644 --- a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs +++ b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs @@ -21,20 +21,20 @@ public class HostStartupInfo /// The default host name for PowerShell Editor Services. Used /// if no host name is specified by the host application. /// - public const string DefaultHostName = "PowerShell Editor Services Host"; + private const string DefaultHostName = "PowerShell Editor Services Host"; /// /// The default host ID for PowerShell Editor Services. Used /// for the host-specific profile path if no host ID is specified. /// - public const string DefaultHostProfileId = "Microsoft.PowerShellEditorServices"; + private const string DefaultHostProfileId = "Microsoft.PowerShellEditorServices"; /// /// The default host version for PowerShell Editor Services. If /// no version is specified by the host application, we use 0.0.0 /// to indicate a lack of version. /// - public static readonly Version DefaultHostVersion = new Version("0.0.0"); + private static readonly Version s_defaultHostVersion = new Version("0.0.0"); #endregion @@ -56,15 +56,7 @@ public class HostStartupInfo /// public Version Version { get; } - /// - /// The path to the single user PowerShell profile. - /// - public string CurrentUserProfilePath { get; } - - /// - /// The path to the shared PowerShell profile. - /// - public string AllUsersProfilePath { get; } + public ProfilePathInfo ProfilePaths { get; } /// /// Any feature flags enabled at startup. @@ -134,8 +126,7 @@ public HostStartupInfo( string profileId, Version version, PSHost psHost, - string allUsersProfilePath, - string currentUsersProfilePath, + ProfilePathInfo profilePaths, IReadOnlyList featureFlags, IReadOnlyList additionalModules, string logPath, @@ -145,10 +136,9 @@ public HostStartupInfo( { Name = name ?? DefaultHostName; ProfileId = profileId ?? DefaultHostProfileId; - Version = version ?? DefaultHostVersion; + Version = version ?? s_defaultHostVersion; PSHost = psHost; - AllUsersProfilePath = allUsersProfilePath; - CurrentUserProfilePath = currentUsersProfilePath; + ProfilePaths = profilePaths; FeatureFlags = featureFlags ?? Array.Empty(); AdditionalModules = additionalModules ?? Array.Empty(); LogPath = logPath; @@ -159,4 +149,27 @@ public HostStartupInfo( #endregion } + + public class ProfilePathInfo + { + public ProfilePathInfo( + string currentUserAllHosts, + string currentUserCurrentHost, + string allUsersAllHosts, + string allUsersCurrentHost) + { + CurrentUserAllHosts = currentUserAllHosts; + CurrentUserCurrentHost = currentUserCurrentHost; + AllUsersAllHosts = allUsersAllHosts; + AllUsersCurrentHost = allUsersCurrentHost; + } + + public string CurrentUserAllHosts { get; } + + public string CurrentUserCurrentHost { get; } + + public string AllUsersAllHosts { get; } + + public string AllUsersCurrentHost { get; } + } } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index 0f4cee21c..52b7e509d 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -64,7 +64,7 @@ static PowerShellContextService() private RunspaceDetails initialRunspace; private SessionDetails mostRecentSessionDetails; - private ProfilePaths profilePaths; + private ProfilePathInfo profilePaths; private IVersionSpecificOperations versionSpecificOperations; @@ -204,8 +204,7 @@ public static PowerShellContextService Create( logger); Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); - var profilePaths = new ProfilePaths(hostDetails.ProfileId, hostDetails.AllUsersProfilePath, hostDetails.CurrentUserProfilePath); - powerShellContext.Initialize(profilePaths, initialRunspace, true, hostUserInterface); + powerShellContext.Initialize(hostDetails.ProfilePaths, initialRunspace, true, hostUserInterface); powerShellContext.ImportCommandsModuleAsync(); @@ -286,11 +285,11 @@ public static Runspace CreateRunspace(PSHost psHost) /// The initial runspace to use for this instance. /// If true, the PowerShellContext owns this runspace. public void Initialize( - ProfilePaths profilePaths, + ProfilePathInfo profilePaths, Runspace initialRunspace, bool ownsInitialRunspace) { - this.Initialize(profilePaths, initialRunspace, ownsInitialRunspace, null); + this.Initialize(profilePaths, initialRunspace, ownsInitialRunspace, consoleHost: null); } /// @@ -302,7 +301,7 @@ public void Initialize( /// If true, the PowerShellContext owns this runspace. /// An IHostOutput implementation. Optional. public void Initialize( - ProfilePaths profilePaths, + ProfilePathInfo profilePaths, Runspace initialRunspace, bool ownsInitialRunspace, IHostOutput consoleHost) @@ -330,7 +329,7 @@ public void Initialize( this.LocalPowerShellVersion, RunspaceLocation.Local, RunspaceContext.Original, - null); + connectionString: null); this.CurrentRunspace = this.initialRunspace; // Write out the PowerShell version for tracking purposes @@ -366,7 +365,7 @@ public void Initialize( // Set the $profile variable in the runspace this.profilePaths = profilePaths; - if (this.profilePaths != null) + if (profilePaths != null) { this.SetProfileVariableInCurrentRunspace(profilePaths); } @@ -1140,7 +1139,7 @@ public async Task LoadHostProfilesAsync() if (this.profilePaths != null) { // Load any of the profile paths that exist - foreach (var profilePath in this.profilePaths.GetLoadableProfilePaths()) + foreach (var profilePath in GetLoadableProfilePaths(this.profilePaths)) { PSCommand command = new PSCommand(); command.AddCommand(profilePath, false); @@ -2208,7 +2207,7 @@ private SessionDetails GetSessionDetailsInNestedPipeline() }); } - private void SetProfileVariableInCurrentRunspace(ProfilePaths profilePaths) + private void SetProfileVariableInCurrentRunspace(ProfilePathInfo profilePaths) { // Create the $profile variable PSObject profile = new PSObject(profilePaths.CurrentUserCurrentHost); @@ -2267,6 +2266,22 @@ private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs ar } } + private static IEnumerable GetLoadableProfilePaths(ProfilePathInfo profilePaths) + { + if (profilePaths == null) + { + yield break; + } + + foreach (string path in new [] { profilePaths.AllUsersAllHosts, profilePaths.AllUsersCurrentHost, profilePaths.CurrentUserAllHosts, profilePaths.CurrentUserCurrentHost }) + { + if (path != null && File.Exists(path)) + { + yield return path; + } + } + } + #endregion #region Events diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/ProfilePaths.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/ProfilePaths.cs deleted file mode 100644 index 2c16be311..000000000 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/ProfilePaths.cs +++ /dev/null @@ -1,108 +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.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext -{ - /// - /// Provides profile path resolution behavior relative to the name - /// of a particular PowerShell host. - /// - public class ProfilePaths - { - #region Constants - - /// - /// The file name for the "all hosts" profile. Also used as the - /// suffix for the host-specific profile filenames. - /// - public const string AllHostsProfileName = "profile.ps1"; - - #endregion - - #region Properties - - /// - /// Gets the profile path for all users, all hosts. - /// - public string AllUsersAllHosts { get; private set; } - - /// - /// Gets the profile path for all users, current host. - /// - public string AllUsersCurrentHost { get; private set; } - - /// - /// Gets the profile path for the current user, all hosts. - /// - public string CurrentUserAllHosts { get; private set; } - - /// - /// Gets the profile path for the current user and host. - /// - public string CurrentUserCurrentHost { get; private set; } - - #endregion - - #region Public Methods - - /// - /// Creates a new instance of the ProfilePaths class. - /// - /// - /// The identifier of the host used in the host-specific X_profile.ps1 filename. - /// - /// The base path to use for constructing AllUsers profile paths. - /// The base path to use for constructing CurrentUser profile paths. - public ProfilePaths( - string hostProfileId, - string baseAllUsersPath, - string baseCurrentUserPath) - { - this.Initialize(hostProfileId, baseAllUsersPath, baseCurrentUserPath); - } - - private void Initialize( - string hostProfileId, - string baseAllUsersPath, - string baseCurrentUserPath) - { - string currentHostProfileName = - string.Format( - "{0}_{1}", - hostProfileId, - AllHostsProfileName); - - this.AllUsersCurrentHost = Path.Combine(baseAllUsersPath, currentHostProfileName); - this.CurrentUserCurrentHost = Path.Combine(baseCurrentUserPath, currentHostProfileName); - this.AllUsersAllHosts = Path.Combine(baseAllUsersPath, AllHostsProfileName); - this.CurrentUserAllHosts = Path.Combine(baseCurrentUserPath, AllHostsProfileName); - } - - /// - /// Gets the list of profile paths that exist on the filesystem. - /// - /// An IEnumerable of profile path strings to be loaded. - public IEnumerable GetLoadableProfilePaths() - { - var profilePaths = - new string[] - { - this.AllUsersAllHosts, - this.AllUsersCurrentHost, - this.CurrentUserAllHosts, - this.CurrentUserCurrentHost - }; - - return profilePaths.Where(p => File.Exists(p)); - } - - #endregion - } -} - From 267f5d236e6b8684e512bac58ee4741a55c1d8e8 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Mon, 2 Dec 2019 14:48:40 -0800 Subject: [PATCH 28/62] Fix host logging --- .../EditorServicesLoader.cs | 21 ++++- .../HostLogger.cs | 92 +++++++++++++++++++ .../PsesLoadContext.cs | 11 ++- .../StartEditorServicesCommand.cs | 58 +++++++++++- .../Hosting/EditorServicesServerFactory.cs | 2 +- .../PowerShellEditorServices.csproj | 3 +- 6 files changed, 172 insertions(+), 15 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 569511f80..e31d3a7a2 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -51,7 +51,14 @@ public static EditorServicesLoader Create( logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for new AssemblyLoadContext dependency loading"); - var psesLoadContext = new PsesLoadContext(logger, s_psesDependencyDirPath); + var psesLoadContext = new PsesLoadContext(s_psesDependencyDirPath); + + AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) => + { + logger.Log( + PsesLogLevel.Diagnostic, + $"Assembly {args.LoadedAssembly.GetName()} loaded into load context '{AssemblyLoadContext.GetLoadContext(args.LoadedAssembly)}'"); + }; AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext defaultLoadContext, AssemblyName asmName) => { @@ -72,6 +79,14 @@ public static EditorServicesLoader Create( #else // In .NET Framework we add an event here to redirect dependency loading in the current AppDomain for PSES' dependencies logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for dependency loading"); + + AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) => + { + logger.Log( + PsesLogLevel.Diagnostic, + $"Assembly {args.LoadedAssembly.GetName()} loaded"); + }; + AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs args) => { logger.Log(PsesLogLevel.Diagnostic, $"Assembly resolve event fired for {args.Name}"); @@ -220,8 +235,8 @@ private void LogHostInformation() - REPL setting: {_hostConfig.ConsoleRepl} - Session details path: {_hostConfig.SessionDetailsPath} - Bundled modules path: {_hostConfig.BundledModulePath} - - Additional modules: {_hostConfig.AdditionalModules} - - Feature flags: {_hostConfig.FeatureFlags} + - Additional modules: {(_hostConfig.AdditionalModules == null ? "" : string.Join(", ", _hostConfig.AdditionalModules))} + - Feature flags: {(_hostConfig.FeatureFlags == null ? "" : string.Join(", ", _hostConfig.FeatureFlags))} - Log path: {_hostConfig.LogPath} - Minimum log level: {_hostConfig.LogLevel} diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index 05bb608a7..dcc238311 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -3,11 +3,15 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using Serilog; using System; using System.Collections; using System.Collections.Concurrent; +using System.IO; using System.Management.Automation.Host; using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Hosting { @@ -236,4 +240,92 @@ public void OnNext((PsesLogLevel logLevel, string message) value) } } } + + internal class StreamLogger : IObserver<(PsesLogLevel logLevel, string message)>, IDisposable + { + public static StreamLogger CreateWithNewFile(string path) + { + return new StreamLogger( + new StreamWriter( + new FileStream( + path, + FileMode.Create, + FileAccess.Write, + FileShare.Read, + bufferSize: 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan))); + } + + private readonly StreamWriter _fileWriter; + + // This cannot be a bool + // See https://stackoverflow.com/q/6164751 + private int _hasCompleted; + + private IDisposable _unsubscriber; + + public StreamLogger(StreamWriter streamWriter) + { + _hasCompleted = 0; + _fileWriter = streamWriter; + } + + public void OnCompleted() + { + // Ensure we only complete once + if (Interlocked.Exchange(ref _hasCompleted, 1) != 0) + { + return; + } + + _unsubscriber.Dispose(); + _fileWriter.Flush(); + _fileWriter.Close(); + _fileWriter.Dispose(); + } + + public void OnError(Exception error) + { + OnNext((PsesLogLevel.Error, $"Error occurred while logging: {error}")); + } + + public void OnNext((PsesLogLevel logLevel, string message) value) + { + string message = null; + switch (value.logLevel) + { + case PsesLogLevel.Diagnostic: + message = $"[DEBUG]: {value.message}"; + break; + + case PsesLogLevel.Verbose: + message = $"[VERBOSE]: {value.message}"; + break; + + case PsesLogLevel.Normal: + message = $"[INFO]: {value.message}"; + break; + + case PsesLogLevel.Warning: + message = $"[WARN]: {value.message}"; + break; + + case PsesLogLevel.Error: + message = $"[ERROR]: {value.message}"; + break; + }; + + _fileWriter.WriteLineAsync(message); + } + + public void AddUnsubscriber(IDisposable unsubscriber) + { + _unsubscriber = unsubscriber; + } + + public void Dispose() + { + OnCompleted(); + } + } } diff --git a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs index 6afa4df5d..03fee60c6 100644 --- a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs +++ b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.IO; using System.Reflection; using System.Runtime.Loader; @@ -17,23 +18,23 @@ namespace Microsoft.PowerShell.EditorServices.Hosting /// internal class PsesLoadContext : AssemblyLoadContext { - private readonly HostLogger _logger; - private readonly string _dependencyDirPath; - public PsesLoadContext(HostLogger logger, string dependencyDirPath) + public PsesLoadContext(string dependencyDirPath) { - _logger = logger; _dependencyDirPath = dependencyDirPath; } protected override Assembly Load(AssemblyName assemblyName) { + // Since this class is responsible for loading any DLLs in .NET Core, + // we must restrict the code in here to only use core types, + // otherwise we may depend on assembly that we are trying to load and cause a StackOverflowException + string asmPath = Path.Combine(_dependencyDirPath, $"{assemblyName.Name}.dll"); if (File.Exists(asmPath)) { - _logger.Log(PsesLogLevel.Diagnostic, $"Loading assembly '{assemblyName}' in isolated load context"); return LoadFromAssemblyPath(asmPath); } diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 8a1b2a47b..42c58e8b0 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -5,6 +5,7 @@ using Microsoft.PowerShell.Commands; using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Management.Automation; @@ -19,9 +20,17 @@ namespace Microsoft.PowerShell.EditorServices.Hosting [Cmdlet(VerbsLifecycle.Start, "EditorServices", DefaultParameterSetName = "NamedPipe")] public sealed class StartEditorServicesCommand : PSCmdlet { + private readonly List _disposableResources; + + private readonly List _loggerUnsubscribers; + private HostLogger _logger; - private IDisposable _hostLoggerSubscription; + public StartEditorServicesCommand() + { + _disposableResources = new List(); + _loggerUnsubscribers = new List(); + } /// /// The name of the EditorServices host to report @@ -174,9 +183,7 @@ protected override void BeginProcessing() #endif // Set up logging now for use throughout startup - _logger = new HostLogger(LogLevel); - _hostLoggerSubscription = _logger.Subscribe(new PSHostLogger(Host.UI)); - _logger.Log(PsesLogLevel.Diagnostic, "Logger created"); + StartLogging(); } protected override void EndProcessing() @@ -195,7 +202,7 @@ protected override void EndProcessing() var sessionFileWriter = new SessionFileWriter(_logger, SessionDetailsPath); _logger.Log(PsesLogLevel.Diagnostic, "Session file writer created"); - using (var psesLoader = EditorServicesLoader.Create(_logger, editorServicesConfig, sessionFileWriter, loggersToUnsubscribe: new[] { _hostLoggerSubscription })) + using (var psesLoader = EditorServicesLoader.Create(_logger, editorServicesConfig, sessionFileWriter, _loggerUnsubscribers)) { _logger.Log(PsesLogLevel.Verbose, "Loading EditorServices"); psesLoader.LoadAndRunEditorServicesAsync().Wait(); @@ -211,6 +218,47 @@ protected override void EndProcessing() ThrowTerminatingError(new ErrorRecord(e, "PowerShellEditorServicesError", ErrorCategory.NotSpecified, this)); } + finally + { + foreach (IDisposable disposableResource in _disposableResources) + { + disposableResource.Dispose(); + } + } + } + + private void StartLogging() + { + _logger = new HostLogger(LogLevel); + + // We need to not write log messages to Stdio + // if it's being used as a protocol transport + if (!Stdio) + { + var hostLogger = new PSHostLogger(Host.UI); + _loggerUnsubscribers.Add(_logger.Subscribe(hostLogger)); + } + + string logPath = Path.Combine(GetLogDirPath(), "StartEditorServices.log"); + var fileLogger = StreamLogger.CreateWithNewFile(logPath); + _disposableResources.Add(fileLogger); + IDisposable fileLoggerUnsubscriber = _logger.Subscribe(fileLogger); + fileLogger.AddUnsubscriber(fileLoggerUnsubscriber); + _loggerUnsubscribers.Add(fileLoggerUnsubscriber); + + _logger.Log(PsesLogLevel.Diagnostic, "Logging started"); + } + + private string GetLogDirPath() + { + if (!string.IsNullOrEmpty(LogPath)) + { + return Path.GetDirectoryName(LogPath); + } + + return Path.GetDirectoryName( + Path.GetDirectoryName( + Assembly.GetExecutingAssembly().Location)); } private void RemovePSReadLineForStartup() diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index 2a6f8fdf5..4dc4ae013 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -35,7 +35,7 @@ public static EditorServicesServerFactory Create(string logPath, int minimumLogL .MinimumLevel.Verbose() .CreateLogger(); - ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(); // Hook up logging from the host so that its recorded in the log file hostLogger.Subscribe(new HostLoggerAdapter(loggerFactory)); diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index 1cb86ecfa..d44ac868e 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -1,4 +1,4 @@ - + @@ -34,6 +34,7 @@ + From 51491af8ab166103b694b17a23fb84541a6887a9 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Mon, 2 Dec 2019 15:24:40 -0800 Subject: [PATCH 29/62] Improve asm loading logging --- .../EditorServicesLoader.cs | 4 ++-- .../PsesLoadContext.cs | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index e31d3a7a2..bc953f647 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -57,7 +57,7 @@ public static EditorServicesLoader Create( { logger.Log( PsesLogLevel.Diagnostic, - $"Assembly {args.LoadedAssembly.GetName()} loaded into load context '{AssemblyLoadContext.GetLoadContext(args.LoadedAssembly)}'"); + $"Loaded into load context {AssemblyLoadContext.GetLoadContext(args.LoadedAssembly)}: {args.LoadedAssembly}"); }; AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext defaultLoadContext, AssemblyName asmName) => @@ -84,7 +84,7 @@ public static EditorServicesLoader Create( { logger.Log( PsesLogLevel.Diagnostic, - $"Assembly {args.LoadedAssembly.GetName()} loaded"); + $"Loaded {args.LoadedAssembly.GetName()}"); }; AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs args) => diff --git a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs index 03fee60c6..e42e0f9f7 100644 --- a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs +++ b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs @@ -23,6 +23,8 @@ internal class PsesLoadContext : AssemblyLoadContext public PsesLoadContext(string dependencyDirPath) { _dependencyDirPath = dependencyDirPath; + + TrySetName("PsesLoadContext"); } protected override Assembly Load(AssemblyName assemblyName) @@ -40,5 +42,25 @@ protected override Assembly Load(AssemblyName assemblyName) return null; } + + private void TrySetName(string name) + { + try + { + // This field only exists in .NET Core 3+, but helps logging + FieldInfo nameBackingField = typeof(AssemblyLoadContext).GetField( + "_name", + BindingFlags.NonPublic | BindingFlags.Instance); + + if (nameBackingField != null) + { + nameBackingField.SetValue(this, name); + } + } + catch + { + // Do nothing -- we did our best + } + } } } From 8582be454940bea899d94486801761d973c4ab67 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Mon, 2 Dec 2019 15:24:50 -0800 Subject: [PATCH 30/62] Attempt async logging --- .../EditorServicesRunner.cs | 1 + .../Hosting/EditorServicesServerFactory.cs | 10 ++++++++-- .../PowerShellEditorServices.csproj | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index deb7c3f90..68c765e7e 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -80,6 +80,7 @@ public async Task RunUntilShutdown() public void Dispose() { + _serverFactory.Dispose(); } /// diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index 4dc4ae013..258795fed 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -18,7 +18,7 @@ namespace Microsoft.PowerShell.EditorServices.Hosting /// so that the host assembly can construct the LSP and debug servers /// without taking logging or dependency injection dependencies directly. /// - internal class EditorServicesServerFactory + internal class EditorServicesServerFactory : IDisposable { /// /// Create a new Editor Services factory. @@ -31,7 +31,7 @@ public static EditorServicesServerFactory Create(string logPath, int minimumLogL { Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() - .WriteTo.File(logPath) + .WriteTo.Async(config => config.File(logPath)) .MinimumLevel.Verbose() .CreateLogger(); @@ -106,5 +106,11 @@ public PsesDebugServer CreateDebugServerForTempSession(Stream inputStream, Strea { return PsesDebugServer.CreateForTempSession(_loggerFactory, inputStream, outputStream, hostStartupInfo); } + + public void Dispose() + { + Log.CloseAndFlush(); + _loggerFactory.Dispose(); + } } } diff --git a/src/PowerShellEditorServices/PowerShellEditorServices.csproj b/src/PowerShellEditorServices/PowerShellEditorServices.csproj index d44ac868e..bf7e9062b 100644 --- a/src/PowerShellEditorServices/PowerShellEditorServices.csproj +++ b/src/PowerShellEditorServices/PowerShellEditorServices.csproj @@ -34,7 +34,7 @@ - + From d7c6981eaa90712b217caa5628c1ed6c9f2d5f78 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 2 Dec 2019 16:08:48 -0800 Subject: [PATCH 31/62] Fix UnixConsoleEcho dependencies in build --- PowerShellEditorServices.build.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 93feeccb3..53b9495b9 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -278,10 +278,15 @@ task LayoutModule -After Build { # Copy Third Party Notices.txt to module folder Copy-Item -Force -Path "$PSScriptRoot\Third Party Notices.txt" -Destination $psesOutputPath + # Copy UnixConsoleEcho native libraries + Copy-Item -Path "$script:PsesOutput/runtimes/osx-64/native/*" -Destination $psesDepsPath + Copy-Item -Path "$script:PsesOutput/runtimes/linux-64/native/*" -Destination $psesDepsPath + $includedDlls = [System.Collections.Generic.HashSet[string]]::new() foreach ($psesComponent in Get-ChildItem $script:PsesOutput) { - if ($psesComponent.Name -eq 'System.Management.Automation.dll') + if ($psesComponent.Name -eq 'System.Management.Automation.dll' -or + $psesComponent.Name -eq 'System.Runtime.InteropServices.RuntimeInformation.dll') { continue } From 7680ccc540ff668beb0916371d6b2a717a73a642 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 2 Dec 2019 16:36:08 -0800 Subject: [PATCH 32/62] Update src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs Co-Authored-By: Patrick Meinecke --- .../StartEditorServicesCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 42c58e8b0..3ba4b41be 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -33,7 +33,7 @@ public StartEditorServicesCommand() } /// - /// The name of the EditorServices host to report + /// The name of the EditorServices host to report. /// [Parameter(Mandatory = true)] [ValidateNotNullOrEmpty] From fb703c190678313a9ed292d1bc4ed2541769aae8 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 2 Dec 2019 17:46:18 -0800 Subject: [PATCH 33/62] Address some feedback --- src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs | 4 ++-- .../PowerShellEditorServices.Hosting.csproj | 4 +--- .../StartEditorServicesCommand.cs | 2 +- src/PowerShellEditorServices/Hosting/HostStartupInfo.cs | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs index 414baafb5..5a68efc97 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs @@ -16,9 +16,9 @@ public enum ConsoleReplKind /// No console REPL - there will be no interactive console available. None = 0, /// Use a REPL with the legacy readline implementation. This is generally used when PSReadLine is unavailable. - LegacyReadLine, + LegacyReadLine = 1, /// Use a REPL with the PSReadLine module for console interaction. - PSReadLine, + PSReadLine = 2, } /// diff --git a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj index eaf7e9da6..6f6676e95 100644 --- a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj +++ b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj @@ -13,9 +13,7 @@ - - ..\..\..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio\2019\Preview\MSBuild\Microsoft\Microsoft.NET.Build.Extensions\net461\lib\System.Runtime.InteropServices.RuntimeInformation.dll - + diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 3ba4b41be..7aaf093ff 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -264,7 +264,7 @@ private string GetLogDirPath() private void RemovePSReadLineForStartup() { _logger.Log(PsesLogLevel.Verbose, "Removing PSReadLine"); - using (var pwsh = SMA.PowerShell.Create()) + using (var pwsh = SMA.PowerShell.Create(RunspaceMode.CurrentRunspace)) { bool hasPSReadLine = pwsh.AddCommand(new CmdletInfo("Microsoft.PowerShell.Core\\Get-Module", typeof(GetModuleCommand))) .AddParameter("Name", "PSReadLine") diff --git a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs index f4260e8a5..a83ff2850 100644 --- a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs +++ b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs @@ -34,7 +34,7 @@ public class HostStartupInfo /// no version is specified by the host application, we use 0.0.0 /// to indicate a lack of version. /// - private static readonly Version s_defaultHostVersion = new Version("0.0.0"); + private static readonly Version s_defaultHostVersion = new Version(0, 0, 0); #endregion From 3929b81bb0c138a143494e6b1a865996cc0ab64c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 3 Dec 2019 13:15:43 -0800 Subject: [PATCH 34/62] Fix Serilog file logging issue --- .../EditorServicesLoader.cs | 3 ++- src/PowerShellEditorServices.Hosting/HostLogger.cs | 2 ++ .../PsesLoadContext.cs | 12 +++++++++++- .../Hosting/EditorServicesServerFactory.cs | 12 +++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index bc953f647..cdcb16ac2 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -10,10 +10,11 @@ using System.Collections.Generic; using SMA = System.Management.Automation; using System.Runtime.InteropServices; -using Microsoft.Win32; #if CoreCLR using System.Runtime.Loader; +#else +using Microsoft.Win32; #endif namespace Microsoft.PowerShell.EditorServices.Hosting diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index dcc238311..f52c3a2b6 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -17,6 +17,8 @@ namespace Microsoft.PowerShell.EditorServices.Hosting { /// /// User-facing log level for editor services configuration. + /// The underlying values of this enum align to both Microsoft.Logging.Extensions.LogLevel + /// and Serilog.Events.LogEventLevel. /// public enum PsesLogLevel { diff --git a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs index e42e0f9f7..3a7cd4cd3 100644 --- a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs +++ b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs @@ -18,6 +18,9 @@ namespace Microsoft.PowerShell.EditorServices.Hosting /// internal class PsesLoadContext : AssemblyLoadContext { + private static readonly string s_psHome = Path.GetDirectoryName( + Assembly.GetEntryAssembly().Location); + private readonly string _dependencyDirPath; public PsesLoadContext(string dependencyDirPath) @@ -33,7 +36,14 @@ protected override Assembly Load(AssemblyName assemblyName) // we must restrict the code in here to only use core types, // otherwise we may depend on assembly that we are trying to load and cause a StackOverflowException - string asmPath = Path.Combine(_dependencyDirPath, $"{assemblyName.Name}.dll"); + string psHomeAsmPath = Path.Join(s_psHome, $"{assemblyName.Name}.dll"); + + if (File.Exists(psHomeAsmPath)) + { + return null; + } + + string asmPath = Path.Join(_dependencyDirPath, $"{assemblyName.Name}.dll"); if (File.Exists(asmPath)) { diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index 258795fed..56232c96c 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -7,9 +7,15 @@ using Microsoft.PowerShell.EditorServices.Server; using PowerShellEditorServices.Logging; using Serilog; +using Serilog.Events; using System; +using System.Diagnostics; using System.IO; +#if DEBUG +using Serilog.Debugging; +#endif + namespace Microsoft.PowerShell.EditorServices.Hosting { /// @@ -32,9 +38,13 @@ public static EditorServicesServerFactory Create(string logPath, int minimumLogL Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() .WriteTo.Async(config => config.File(logPath)) - .MinimumLevel.Verbose() + .MinimumLevel.Is((LogEventLevel)minimumLogLevel) .CreateLogger(); +#if DEBUG + SelfLog.Enable(msg => Debug.WriteLine(msg)); +#endif + ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(); // Hook up logging from the host so that its recorded in the log file From 2c3f4a274bf0fe4751cbda1977219ab67e23779f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 3 Dec 2019 13:28:03 -0800 Subject: [PATCH 35/62] Add PowerShell version to log --- .../EditorServicesLoader.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index cdcb16ac2..0475a1e74 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -256,6 +256,11 @@ private void LogHostInformation() - PowerShell output encoding: {GetPSOutputEncoding()} "); + _logger.Log(PsesLogLevel.Verbose, $@" +== PowerShell Details == + - PowerShell version: {GetPowerShellVersion()} +"); + LogOperatingSystemDetails(); } @@ -267,6 +272,14 @@ private string GetPSOutputEncoding() } } + private string GetPowerShellVersion() + { + using (var pwsh = SMA.PowerShell.Create()) + { + return pwsh.AddScript("$PSVersionTable.PSVersion").Invoke()[0].ToString(); + } + } + private void LogOperatingSystemDetails() { _logger.Log(PsesLogLevel.Verbose, $@" From c0e0e64d55d42e92a59b5c93bd207bab97a18abc Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 3 Dec 2019 13:50:44 -0800 Subject: [PATCH 36/62] Fix log flushing issue --- .../HostLogger.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index f52c3a2b6..5e6aa7d70 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -3,15 +3,13 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Serilog; using System; -using System.Collections; using System.Collections.Concurrent; using System.IO; using System.Management.Automation.Host; using System.Runtime.CompilerServices; +using System.Text; using System.Threading; -using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Hosting { @@ -247,15 +245,15 @@ internal class StreamLogger : IObserver<(PsesLogLevel logLevel, string message)> { public static StreamLogger CreateWithNewFile(string path) { - return new StreamLogger( - new StreamWriter( - new FileStream( - path, - FileMode.Create, - FileAccess.Write, - FileShare.Read, - bufferSize: 4096, - FileOptions.Asynchronous | FileOptions.SequentialScan))); + var fileStream = new FileStream( + path, + FileMode.Create, + FileAccess.Write, + FileShare.Read, + bufferSize: 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan); + + return new StreamLogger(new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true))); } private readonly StreamWriter _fileWriter; @@ -269,6 +267,7 @@ public static StreamLogger CreateWithNewFile(string path) public StreamLogger(StreamWriter streamWriter) { _hasCompleted = 0; + streamWriter.AutoFlush = true; _fileWriter = streamWriter; } From a14a2b618cc16a7bda54a39b03b8887db9c84780 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 3 Dec 2019 14:58:39 -0800 Subject: [PATCH 37/62] Configure PSReadLine correctly --- .../EditorServicesRunner.cs | 16 ++--- .../Hosting/EditorServicesServerFactory.cs | 27 ++++++-- .../Server/PsesDebugServer.cs | 67 ++++--------------- .../Server/PsesLanguageServer.cs | 2 - 4 files changed, 43 insertions(+), 69 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 68c765e7e..946c14abd 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -126,7 +126,7 @@ private async Task CreateEditorServicesAndRunUntilShutdown() Task debugServerCreation = null; if (creatingDebugServer) { - debugServerCreation = CreateDebugServerWithLanguageServer(languageServer); + debugServerCreation = CreateDebugServerWithLanguageServer(languageServer, usePSReadLine: _config.ConsoleRepl == ConsoleReplKind.PSReadLine); } languageServer.StartAsync(); @@ -171,10 +171,10 @@ private async Task StartDebugServer(Task debugServerCreation) return; } - private Task RestartDebugServer(PsesDebugServer debugServer) + private Task RestartDebugServer(PsesDebugServer debugServer, bool usePSReadLine) { _logger.Log(PsesLogLevel.Diagnostic, "Restarting debug server"); - Task debugServerCreation = RecreateDebugServer(debugServer); + Task debugServerCreation = RecreateDebugServer(debugServer, usePSReadLine); return StartDebugServer(debugServerCreation); } @@ -187,22 +187,22 @@ private async Task CreateLanguageServer(HostStartupInfo host return _serverFactory.CreateLanguageServer(inStream, outStream, hostDetails); } - private async Task CreateDebugServerWithLanguageServer(PsesLanguageServer languageServer) + private async Task CreateDebugServerWithLanguageServer(PsesLanguageServer languageServer, bool usePSReadLine) { _logger.Log(PsesLogLevel.Verbose, $"Creating debug adapter transport with endpoint {_config.DebugServiceTransport.Endpoint}"); (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); _logger.Log(PsesLogLevel.Diagnostic, "Creating debug adapter"); - return _serverFactory.CreateDebugServerWithLanguageServer(inStream, outStream, languageServer); + return _serverFactory.CreateDebugServerWithLanguageServer(inStream, outStream, languageServer, usePSReadLine); } - private async Task RecreateDebugServer(PsesDebugServer debugServer) + private async Task RecreateDebugServer(PsesDebugServer debugServer, bool usePSReadLine) { _logger.Log(PsesLogLevel.Diagnostic, "Recreating debug adapter transport"); (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); _logger.Log(PsesLogLevel.Diagnostic, "Recreating debug adapter"); - return _serverFactory.RecreateDebugServer(inStream, outStream, debugServer); + return _serverFactory.RecreateDebugServer(inStream, outStream, debugServer, usePSReadLine); } private async Task CreateDebugServerForTempSession(HostStartupInfo hostDetails) @@ -251,7 +251,7 @@ private void DebugServer_OnSessionEnded(object sender, EventArgs args) _alreadySubscribedDebug = false; Task.Run(() => { - RestartDebugServer(oldServer); + RestartDebugServer(oldServer, usePSReadLine: _config.ConsoleRepl == ConsoleReplKind.PSReadLine); }); } } diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index 56232c96c..710891a48 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -11,6 +11,8 @@ using System; using System.Diagnostics; using System.IO; +using Microsoft.Extensions.DependencyInjection; +using OmniSharp.Extensions.LanguageServer.Server; #if DEBUG using Serilog.Debugging; @@ -88,9 +90,9 @@ public PsesLanguageServer CreateLanguageServer( /// The protocol transport output stream. /// /// A new, unstarted debug server instance. - public PsesDebugServer CreateDebugServerWithLanguageServer(Stream inputStream, Stream outputStream, PsesLanguageServer languageServer) + public PsesDebugServer CreateDebugServerWithLanguageServer(Stream inputStream, Stream outputStream, PsesLanguageServer languageServer, bool usePSReadLine) { - return PsesDebugServer.CreateWithLanguageServerServices(_loggerFactory, inputStream, outputStream, languageServer.LanguageServer.Services); + return new PsesDebugServer(_loggerFactory, inputStream, outputStream, languageServer.LanguageServer.Services, useTempSession: false, usePSReadLine); } /// @@ -100,9 +102,9 @@ public PsesDebugServer CreateDebugServerWithLanguageServer(Stream inputStream, S /// The protocol transport output stream. /// The old debug server to recreate. /// - public PsesDebugServer RecreateDebugServer(Stream inputStream, Stream outputStream, PsesDebugServer debugServer) + public PsesDebugServer RecreateDebugServer(Stream inputStream, Stream outputStream, PsesDebugServer debugServer, bool usePSReadLine) { - return PsesDebugServer.CreateWithLanguageServerServices(_loggerFactory, inputStream, outputStream, debugServer.ServiceProvider); + return new PsesDebugServer(_loggerFactory, inputStream, outputStream, debugServer.ServiceProvider, useTempSession: false, usePSReadLine); } /// @@ -114,7 +116,22 @@ public PsesDebugServer RecreateDebugServer(Stream inputStream, Stream outputStre /// public PsesDebugServer CreateDebugServerForTempSession(Stream inputStream, Stream outputStream, HostStartupInfo hostStartupInfo) { - return PsesDebugServer.CreateForTempSession(_loggerFactory, inputStream, outputStream, hostStartupInfo); + var serviceProvider = new ServiceCollection() + .AddLogging(builder => builder + .ClearProviders() + .AddSerilog() + .SetMinimumLevel(LogLevel.Trace)) + .AddSingleton(provider => null) + .AddPsesLanguageServices(hostStartupInfo) + .BuildServiceProvider(); + + return new PsesDebugServer( + _loggerFactory, + inputStream, + outputStream, + serviceProvider, + useTempSession: true, + usePSReadLine: hostStartupInfo.ConsoleReplEnabled && !hostStartupInfo.UsesLegacyReadLine); } public void Dispose() diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 14eb3af94..25614b866 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -4,18 +4,15 @@ // using System; -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.PowerShell.EditorServices.Handlers; -using Microsoft.PowerShell.EditorServices.Hosting; using Microsoft.PowerShell.EditorServices.Services; using OmniSharp.Extensions.DebugAdapter.Protocol.Serialization; using OmniSharp.Extensions.JsonRpc; using OmniSharp.Extensions.LanguageServer.Server; -using Serilog; namespace Microsoft.PowerShell.EditorServices.Server { @@ -24,66 +21,24 @@ namespace Microsoft.PowerShell.EditorServices.Server /// internal class PsesDebugServer : IDisposable { - protected readonly ILoggerFactory _loggerFactory; private readonly Stream _inputStream; private readonly Stream _outputStream; private readonly bool _useTempSession; + private readonly bool _usePSReadLine; + private readonly TaskCompletionSource _serverStopped; private IJsonRpcServer _jsonRpcServer; - private PowerShellContextService _powerShellContextService; - private readonly TaskCompletionSource _serverStopped; - - /// - /// Create a debug server using existing services from a language server instance. - /// - /// Factory to instantiate loggers with. - /// Protocol transport input stream. - /// Protocol transport output stream. - /// Service provider from the language server to use. - /// - public static PsesDebugServer CreateWithLanguageServerServices( - ILoggerFactory loggerFactory, - Stream inputStream, - Stream outputStream, - IServiceProvider languageServerServiceProvider) - { - return new PsesDebugServer(loggerFactory, inputStream, outputStream, languageServerServiceProvider, useTempSession: false); - } - - /// - /// Create a debug server instantiating services from a host configuration. - /// - /// Factory to create loggers with. - /// Protocol transport input stream. - /// Protocol transport output stream. - /// The host configuration to create services with. - /// - public static PsesDebugServer CreateForTempSession( - ILoggerFactory loggerFactory, - Stream inputStream, - Stream outputStream, - HostStartupInfo hostDetails) - { - var serviceProvider = new ServiceCollection() - .AddLogging(builder => builder - .ClearProviders() - .AddSerilog() - .SetMinimumLevel(LogLevel.Trace)) - .AddSingleton(provider => null) - .AddPsesLanguageServices(hostDetails) - .BuildServiceProvider(); - - return new PsesDebugServer(loggerFactory, inputStream, outputStream, serviceProvider, useTempSession: true); - } + protected readonly ILoggerFactory _loggerFactory; - private PsesDebugServer( + public PsesDebugServer( ILoggerFactory factory, Stream inputStream, Stream outputStream, IServiceProvider serviceProvider, - bool useTempSession) + bool useTempSession, + bool usePSReadLine) { _loggerFactory = factory; _inputStream = inputStream; @@ -91,6 +46,7 @@ private PsesDebugServer( ServiceProvider = serviceProvider; _useTempSession = useTempSession; _serverStopped = new TaskCompletionSource(); + _usePSReadLine = usePSReadLine; } internal IServiceProvider ServiceProvider { get; } @@ -114,9 +70,12 @@ public async Task StartAsync() _powerShellContextService.IsDebugServerActive = true; // Needed to make sure PSReadLine's static properties are initialized in the pipeline thread. - _powerShellContextService - .ExecuteScriptStringAsync("[System.Runtime.CompilerServices.RuntimeHelpers]::RunClassConstructor([Microsoft.PowerShell.PSConsoleReadLine].TypeHandle)") - .Wait(); + if (_usePSReadLine) + { + _powerShellContextService + .ExecuteScriptStringAsync("[System.Runtime.CompilerServices.RuntimeHelpers]::RunClassConstructor([Microsoft.PowerShell.PSConsoleReadLine].TypeHandle)") + .Wait(); + } options.Services = new ServiceCollection() .AddPsesDebugServices(ServiceProvider, this, _useTempSession); diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index 00c4b571e..c0cfa8d8a 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -3,8 +3,6 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using System; -using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; From ed702c2f3d7f351c66df880d697cd6b1f83cd17f Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 3 Dec 2019 15:49:35 -0800 Subject: [PATCH 38/62] Fix log thread collision issue --- .../HostLogger.cs | 36 +++++++++++++++++-- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index 5e6aa7d70..68197b3a8 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -10,6 +10,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace Microsoft.PowerShell.EditorServices.Hosting { @@ -251,13 +252,19 @@ public static StreamLogger CreateWithNewFile(string path) FileAccess.Write, FileShare.Read, bufferSize: 4096, - FileOptions.Asynchronous | FileOptions.SequentialScan); + FileOptions.SequentialScan); return new StreamLogger(new StreamWriter(fileStream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true))); } private readonly StreamWriter _fileWriter; + private readonly BlockingCollection _messageQueue; + + private readonly CancellationTokenSource _cancellationSource; + + private readonly Task _writerTask; + // This cannot be a bool // See https://stackoverflow.com/q/6164751 private int _hasCompleted; @@ -266,9 +273,14 @@ public static StreamLogger CreateWithNewFile(string path) public StreamLogger(StreamWriter streamWriter) { - _hasCompleted = 0; streamWriter.AutoFlush = true; _fileWriter = streamWriter; + _hasCompleted = 0; + _cancellationSource = new CancellationTokenSource(); + _messageQueue = new BlockingCollection(); + + // Start writer listening to queue + Task.Run(RunWriter); } public void OnCompleted() @@ -279,6 +291,10 @@ public void OnCompleted() return; } + _cancellationSource.Cancel(); + + _writerTask.Wait(); + _unsubscriber.Dispose(); _fileWriter.Flush(); _fileWriter.Close(); @@ -316,7 +332,7 @@ public void OnNext((PsesLogLevel logLevel, string message) value) break; }; - _fileWriter.WriteLineAsync(message); + _messageQueue.Add(message); } public void AddUnsubscriber(IDisposable unsubscriber) @@ -328,5 +344,19 @@ public void Dispose() { OnCompleted(); } + + private void RunWriter() + { + try + { + foreach (string logMessage in _messageQueue.GetConsumingEnumerable(_cancellationSource.Token)) + { + _fileWriter.WriteLine(logMessage); + } + } + catch (TaskCanceledException) + { + } + } } } From bfe443028485555916435461d65eb78ff5f29215 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Tue, 3 Dec 2019 15:59:21 -0800 Subject: [PATCH 39/62] Contract log level names --- src/PowerShellEditorServices.Hosting/HostLogger.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index 68197b3a8..9b2cbdd98 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -312,23 +312,23 @@ public void OnNext((PsesLogLevel logLevel, string message) value) switch (value.logLevel) { case PsesLogLevel.Diagnostic: - message = $"[DEBUG]: {value.message}"; + message = $"[DBG]: {value.message}"; break; case PsesLogLevel.Verbose: - message = $"[VERBOSE]: {value.message}"; + message = $"[VRB]: {value.message}"; break; case PsesLogLevel.Normal: - message = $"[INFO]: {value.message}"; + message = $"[INF]: {value.message}"; break; case PsesLogLevel.Warning: - message = $"[WARN]: {value.message}"; + message = $"[WRN]: {value.message}"; break; case PsesLogLevel.Error: - message = $"[ERROR]: {value.message}"; + message = $"[ERR]: {value.message}"; break; }; From a515b64c0475fce517a87838bb4e2f9b00c74096 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Wed, 4 Dec 2019 15:27:17 -0800 Subject: [PATCH 40/62] Fix integration tests --- .../EditorServicesRunner.cs | 5 ++++- .../SessionFileWriter.cs | 2 -- .../StartEditorServicesCommand.cs | 19 +++++++++++++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 946c14abd..2563c75d2 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -119,7 +119,10 @@ private async Task CreateEditorServicesAndRunUntilShutdown() } // Write the integrated console banner - _config.PSHost.UI.WriteLine("\n=== PowerShell Integrated Console ==="); + if (_config.ConsoleRepl != ConsoleReplKind.None) + { + _config.PSHost.UI.WriteLine("\n=== PowerShell Integrated Console ==="); + } PsesLanguageServer languageServer = await CreateLanguageServer(hostStartupInfo).ConfigureAwait(false); diff --git a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs index 7ccc5c416..16696e336 100644 --- a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs +++ b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs @@ -90,8 +90,6 @@ public void WriteSessionStarted(ITransportConfig languageServiceTransport, ITran var sessionObject = new Dictionary { { "status", "started" }, - { "languageServiceTransport", languageServiceTransport.SessionFileTransportName }, - { "debugServiceTransport", debugAdapterTransport.SessionFileTransportName }, }; if (languageServiceTransport != null) diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 7aaf093ff..6aeae5658 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -12,6 +12,13 @@ using System.Reflection; using SMA = System.Management.Automation; +#if DEBUG +using System.Diagnostics; +using System.Threading; + +using Debugger = System.Diagnostics.Debugger; +#endif + namespace Microsoft.PowerShell.EditorServices.Hosting { /// @@ -174,10 +181,10 @@ protected override void BeginProcessing() #if DEBUG if (WaitForDebugger) { - while (!System.Diagnostics.Debugger.IsAttached) + while (!Debugger.IsAttached) { - Console.WriteLine($"PID: {System.Diagnostics.Process.GetCurrentProcess().Id}"); - System.Threading.Thread.Sleep(1000); + Console.WriteLine($"PID: {Process.GetCurrentProcess().Id}"); + Thread.Sleep(1000); } } #endif @@ -213,7 +220,11 @@ protected override void EndProcessing() _logger.LogException("Exception encountered starting EditorServices", e); // Give the user a chance to read the message - Host.UI.WriteLine("\n== Press any key to close terminal =="); + if (!Stdio) + { + Host.UI.WriteLine("\n== Press any key to close terminal =="); + } + Console.ReadKey(); ThrowTerminatingError(new ErrorRecord(e, "PowerShellEditorServicesError", ErrorCategory.NotSpecified, this)); From 877a887ab6b42d6c3edcd62bf40129bc0e6b364e Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Wed, 4 Dec 2019 15:35:02 -0800 Subject: [PATCH 41/62] Fix manifest import in build --- PowerShellEditorServices.build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 53b9495b9..99ad4b493 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -162,7 +162,7 @@ task CreateBuildInfo -Before Build { if ($env:TF_BUILD) { $psd1Path = [System.IO.Path]::Combine($PSScriptRoot, "module", "PowerShellEditorServices", "PowerShellEditorServices.psd1") - $buildVersion = (Import-PowerShellDataFile -LiteralPath $psd1Path).Version + $buildVersion = (Test-ModuleManifest -Path $psd1Path).Version $buildOrigin = "VSTS" } From b7476f34ed61ff1c95981bc97a302520f2732328 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Wed, 4 Dec 2019 15:39:46 -0800 Subject: [PATCH 42/62] Use full version for build info --- PowerShellEditorServices.build.ps1 | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 99ad4b493..cef334000 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -162,7 +162,13 @@ task CreateBuildInfo -Before Build { if ($env:TF_BUILD) { $psd1Path = [System.IO.Path]::Combine($PSScriptRoot, "module", "PowerShellEditorServices", "PowerShellEditorServices.psd1") - $buildVersion = (Test-ModuleManifest -Path $psd1Path).Version + $propsXml = [xml](Get-Content -Raw -LiteralPath $psd1Path) + $propsBody = $propsXml.Project.PropertyGroup + $buildVersion = $propsBody.VersionPrefix + if ($propsBody.VersionSuffix) + { + $buildVersion += '-' + $propsBody.VersionSuffix + } $buildOrigin = "VSTS" } From 9fe2f32d78823dcfeb1cdf3fcc773aab8fd90814 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Wed, 4 Dec 2019 15:45:22 -0800 Subject: [PATCH 43/62] Actually fix buildinfo --- PowerShellEditorServices.build.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index cef334000..51b4cc24c 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -158,17 +158,17 @@ task CreateBuildInfo -Before Build { $buildVersion = "" $buildOrigin = "" - # Set build info fields on build platforms - if ($env:TF_BUILD) + if ($propsBody.VersionSuffix) { - $psd1Path = [System.IO.Path]::Combine($PSScriptRoot, "module", "PowerShellEditorServices", "PowerShellEditorServices.psd1") - $propsXml = [xml](Get-Content -Raw -LiteralPath $psd1Path) + $propsXml = [xml](Get-Content -Raw -LiteralPath "$PSScriptRoot/PowerShellEditorServices.Common.props") $propsBody = $propsXml.Project.PropertyGroup $buildVersion = $propsBody.VersionPrefix - if ($propsBody.VersionSuffix) - { - $buildVersion += '-' + $propsBody.VersionSuffix - } + $buildVersion += '-' + $propsBody.VersionSuffix + } + + # Set build info fields on build platforms + if ($env:TF_BUILD) + { $buildOrigin = "VSTS" } From ed150960d9384f148934f8086a193ec2bdb069dc Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Wed, 4 Dec 2019 15:58:12 -0800 Subject: [PATCH 44/62] Address @SeeminglyScience's remaining feedback --- src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs | 2 +- src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs | 3 ++- src/PowerShellEditorServices.Hosting/HostLogger.cs | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs index 5a68efc97..8e52ceba7 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs @@ -24,7 +24,7 @@ public enum ConsoleReplKind /// /// Configuration for editor services startup. /// - public class EditorServicesConfig + public sealed class EditorServicesConfig { /// /// Create a new editor services config object, diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 0475a1e74..2f111c295 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -8,9 +8,10 @@ using System.Reflection; using System.Threading.Tasks; using System.Collections.Generic; -using SMA = System.Management.Automation; using System.Runtime.InteropServices; +using SMA = System.Management.Automation; + #if CoreCLR using System.Runtime.Loader; #else diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index 9b2cbdd98..2daa40987 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -39,7 +39,7 @@ public class HostLogger : /// /// A simple translation struct to convert PsesLogLevel to an int for backend passthrough. /// - private struct LogObserver : IObserver<(PsesLogLevel logLevel, string message)> + private class LogObserver : IObserver<(PsesLogLevel logLevel, string message)> { private readonly IObserver<(int logLevel, string message)> _observer; @@ -90,6 +90,8 @@ public void Dispose() private readonly ConcurrentQueue<(PsesLogLevel logLevel, string message)> _logMessages; + // The bool value here is meaningless and ignored, + // the ConcurrentDictionary just provides a way to efficiently keep track of subscribers across threads private readonly ConcurrentDictionary, bool> _observers; /// From 33b788134956bc4c9cb89f7311cd67266c938a2d Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Wed, 4 Dec 2019 16:08:09 -0800 Subject: [PATCH 45/62] Code tweaks --- .../EditorServicesConfig.cs | 10 ++++++++-- .../EditorServicesLoader.cs | 10 ++++++++++ src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs | 2 +- .../PsesLoadContext.cs | 1 + 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs index 8e52ceba7..05630324b 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs @@ -118,17 +118,23 @@ public EditorServicesConfig( public struct ProfilePathConfig { /// - /// The path to the profile shared by all users. + /// The path to the profile shared by all users across all PowerShell hosts. /// public string AllUsersAllHosts { get; set; } + /// + /// The path to the profile shared by all users specific to this PSES host. + /// public string AllUsersCurrentHost { get; set; } /// - /// The path to the profile specific to the current user. + /// The path to the profile specific to the current user across all hosts. /// public string CurrentUserAllHosts { get; set; } + /// + /// The path to the profile specific to the current user and to this PSES host. + /// public string CurrentUserCurrentHost { get; set; } } } diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 2f111c295..1217fcb62 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -48,6 +48,16 @@ public static EditorServicesLoader Create( ISessionFileWriter sessionFileWriter, IReadOnlyCollection loggersToUnsubscribe = null) { + if (logger == null) + { + throw new ArgumentNullException(nameof(logger)); + } + + if (logger == null) + { + throw new ArgumentNullException(nameof(hostConfig)); + } + #if CoreCLR // In .NET Core, we add an event here to redirect dependency loading to the new AssemblyLoadContext we load PSES' dependencies into diff --git a/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs b/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs index 626b30d68..01dd713c1 100644 --- a/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs +++ b/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs @@ -116,7 +116,7 @@ public static string GenerateValidNamedPipeName(IReadOnlyCollection pref } while (tries < 10); - throw new Exception("Unable to create named pipe; no available names"); + throw new IOException("Unable to create named pipe; no available names"); } /// diff --git a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs index 3a7cd4cd3..7ed121e05 100644 --- a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs +++ b/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs @@ -27,6 +27,7 @@ public PsesLoadContext(string dependencyDirPath) { _dependencyDirPath = dependencyDirPath; + // Try and set our name in .NET Core 3+ for logging niceness TrySetName("PsesLoadContext"); } From b0c845d2e686d115baa0d80717679a7c500c45c3 Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Fri, 6 Dec 2019 09:28:43 -0800 Subject: [PATCH 46/62] Fix lint issues --- .../EditorServicesLoader.cs | 16 ++++++++++++++-- .../EditorServicesRunner.cs | 1 - .../HostLogger.cs | 8 +++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 1217fcb62..829a47336 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -35,6 +35,18 @@ public sealed class EditorServicesLoader : IDisposable "..", "Common")); + /// + /// Create a new Editor Services loader. + /// + /// The host logger to use. + /// The host configuration to start editor services with. + /// The session file writer to write the session file with. + /// + public static EditorServicesLoader Create( + HostLogger logger, + EditorServicesConfig hostConfig, + ISessionFileWriter sessionFileWriter) => Create(logger, hostConfig, sessionFileWriter, null); + /// /// Create a new Editor Services loader. /// @@ -46,14 +58,14 @@ public static EditorServicesLoader Create( HostLogger logger, EditorServicesConfig hostConfig, ISessionFileWriter sessionFileWriter, - IReadOnlyCollection loggersToUnsubscribe = null) + IReadOnlyCollection loggersToUnsubscribe) { if (logger == null) { throw new ArgumentNullException(nameof(logger)); } - if (logger == null) + if (hostConfig == null) { throw new ArgumentNullException(nameof(hostConfig)); } diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 2563c75d2..a2379829a 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -153,7 +153,6 @@ private async Task RunTempDebugSession(HostStartupInfo hostDetails) await debugServer.StartAsync().ConfigureAwait(false); _logger.Log(PsesLogLevel.Verbose, "Debug server started"); await debugServer.WaitForShutdown().ConfigureAwait(false); - return; } private async Task StartDebugServer(Task debugServerCreation) diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/HostLogger.cs index 2daa40987..6882783e7 100644 --- a/src/PowerShellEditorServices.Hosting/HostLogger.cs +++ b/src/PowerShellEditorServices.Hosting/HostLogger.cs @@ -210,6 +210,8 @@ public PSHostLogger(PSHostUserInterface ui) public void OnCompleted() { + // No-op since there's nothing to close or dispose, + // we just stop writing to the host } public void OnError(Exception error) @@ -240,6 +242,10 @@ public void OnNext((PsesLogLevel logLevel, string message) value) case PsesLogLevel.Error: _ui.WriteErrorLine(value.message); return; + + default: + _ui.WriteLine(value.message); + return; } } } @@ -282,7 +288,7 @@ public StreamLogger(StreamWriter streamWriter) _messageQueue = new BlockingCollection(); // Start writer listening to queue - Task.Run(RunWriter); + _writerTask = Task.Run(RunWriter); } public void OnCompleted() From 6a3c637ac011008269741ff7c6a4df3de6925f7e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 19:23:12 -0800 Subject: [PATCH 47/62] Make simple feedback changes --- .../EditorServicesLoader.cs | 28 +-- .../EditorServicesRunner.cs | 164 +++++++++--------- .../StartEditorServicesCommand.cs | 5 +- .../TransportConfig.cs | 21 +-- .../Hosting/EditorServicesServerFactory.cs | 6 +- .../Server/PsesDebugServer.cs | 5 +- .../Server/PsesLanguageServer.cs | 12 +- .../Server/PsesServiceCollectionExtensions.cs | 4 +- .../PowerShellContextService.cs | 16 +- 9 files changed, 137 insertions(+), 124 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 829a47336..6a71ffb9f 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -77,12 +77,15 @@ public static EditorServicesLoader Create( var psesLoadContext = new PsesLoadContext(s_psesDependencyDirPath); - AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) => + if (hostConfig.LogLevel == PsesLogLevel.Diagnostic) { - logger.Log( - PsesLogLevel.Diagnostic, - $"Loaded into load context {AssemblyLoadContext.GetLoadContext(args.LoadedAssembly)}: {args.LoadedAssembly}"); - }; + AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) => + { + logger.Log( + PsesLogLevel.Diagnostic, + $"Loaded into load context {AssemblyLoadContext.GetLoadContext(args.LoadedAssembly)}: {args.LoadedAssembly}"); + }; + } AssemblyLoadContext.Default.Resolving += (AssemblyLoadContext defaultLoadContext, AssemblyName asmName) => { @@ -104,12 +107,15 @@ public static EditorServicesLoader Create( // In .NET Framework we add an event here to redirect dependency loading in the current AppDomain for PSES' dependencies logger.Log(PsesLogLevel.Verbose, "Adding AssemblyResolve event handler for dependency loading"); - AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) => + if (hostConfig.LogLevel == PsesLogLevel.Diagnostic) { - logger.Log( - PsesLogLevel.Diagnostic, - $"Loaded {args.LoadedAssembly.GetName()}"); - }; + AppDomain.CurrentDomain.AssemblyLoad += (object sender, AssemblyLoadEventArgs args) => + { + logger.Log( + PsesLogLevel.Diagnostic, + $"Loaded {args.LoadedAssembly.GetName()}"); + }; + } AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs args) => { @@ -181,7 +187,7 @@ public async Task LoadAndRunEditorServicesAsync() EditorServicesLoading.LoadEditorServicesForHost(); _logger.Log(PsesLogLevel.Verbose, "Starting EditorServices"); - using (var editorServicesRunner = EditorServicesRunner.Create(_logger, _hostConfig, _sessionFileWriter, _loggersToUnsubscribe)) + using (var editorServicesRunner = new EditorServicesRunner(_logger, _hostConfig, _sessionFileWriter, _loggersToUnsubscribe)) { // The trigger method for Editor Services // We will wait here until Editor Services shuts down diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index a2379829a..2a17e0cf3 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -19,22 +19,6 @@ namespace Microsoft.PowerShell.EditorServices.Hosting /// internal class EditorServicesRunner : IDisposable { - /// - /// Create a new Editor Services runner. - /// - /// The host logger to log through. - /// The startup configuration to use. - /// The session file writer to use. - /// - public static EditorServicesRunner Create( - HostLogger logger, - EditorServicesConfig config, - ISessionFileWriter sessionFileWriter, - IReadOnlyCollection loggersToUnsubscribe) - { - return new EditorServicesRunner(logger, config, sessionFileWriter, loggersToUnsubscribe); - } - private readonly HostLogger _logger; private readonly EditorServicesConfig _config; @@ -47,7 +31,7 @@ public static EditorServicesRunner Create( private bool _alreadySubscribedDebug; - private EditorServicesRunner( + public EditorServicesRunner( HostLogger logger, EditorServicesConfig config, ISessionFileWriter sessionFileWriter, @@ -89,66 +73,67 @@ public void Dispose() /// A task that ends when Editor Services shuts down. private async Task CreateEditorServicesAndRunUntilShutdown() { - bool creatingLanguageServer = _config.LanguageServiceTransport != null; - bool creatingDebugServer = _config.DebugServiceTransport != null; - bool isTempDebugSession = creatingDebugServer && !creatingLanguageServer; - - // Set up information required to instantiate servers - HostStartupInfo hostStartupInfo = CreateHostStartupInfo(); - - // If we just want a temp debug session, run that and do nothing else - if (isTempDebugSession) + try { - await RunTempDebugSession(hostStartupInfo).ConfigureAwait(false); - return; + bool creatingLanguageServer = _config.LanguageServiceTransport != null; + bool creatingDebugServer = _config.DebugServiceTransport != null; + bool isTempDebugSession = creatingDebugServer && !creatingLanguageServer; + + // Set up information required to instantiate servers + HostStartupInfo hostStartupInfo = CreateHostStartupInfo(); + + // If we just want a temp debug session, run that and do nothing else + if (isTempDebugSession) + { + await RunTempDebugSessionAsync(hostStartupInfo).ConfigureAwait(false); + return; + } + + // We want LSP and maybe debugging + // To do that we: + // - Create the LSP server + // - Possibly kick off the debug server creation + // - Start the LSP server + // - Possibly start the debug server + // - Wait for the LSP server to finish + + // Unsubscribe the host logger here so that the integrated console is not polluted with input after the first prompt + _logger.Log(PsesLogLevel.Verbose, "Starting server, deregistering host logger and registering shutdown listener"); + foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) + { + loggerToUnsubscribe.Dispose(); + } + + WriteStartupBanner(); + + PsesLanguageServer languageServer = await CreateLanguageServerAsync(hostStartupInfo).ConfigureAwait(false); + + Task debugServerCreation = null; + if (creatingDebugServer) + { + debugServerCreation = CreateDebugServerWithLanguageServerAsync(languageServer, usePSReadLine: _config.ConsoleRepl == ConsoleReplKind.PSReadLine); + } + + languageServer.StartAsync(); + + if (creatingDebugServer) + { + StartDebugServer(debugServerCreation); + } + + await languageServer.WaitForShutdown().ConfigureAwait(false); } - - // We want LSP and maybe debugging - // To do that we: - // - Create the LSP server - // - Possibly kick off the debug server creation - // - Start the LSP server - // - Possibly start the debug server - // - Wait for the LSP server to finish - - // Unsubscribe the host logger here so that the integrated console is not polluted with input after the first prompt - _logger.Log(PsesLogLevel.Verbose, "Starting server, deregistering host logger and registering shutdown listener"); - foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) + finally { - loggerToUnsubscribe.Dispose(); + // Resubscribe host logger to log shutdown events to the console + _logger.Subscribe(new PSHostLogger(_config.PSHost.UI)); } - - // Write the integrated console banner - if (_config.ConsoleRepl != ConsoleReplKind.None) - { - _config.PSHost.UI.WriteLine("\n=== PowerShell Integrated Console ==="); - } - - PsesLanguageServer languageServer = await CreateLanguageServer(hostStartupInfo).ConfigureAwait(false); - - Task debugServerCreation = null; - if (creatingDebugServer) - { - debugServerCreation = CreateDebugServerWithLanguageServer(languageServer, usePSReadLine: _config.ConsoleRepl == ConsoleReplKind.PSReadLine); - } - - languageServer.StartAsync(); - - if (creatingDebugServer) - { - StartDebugServer(debugServerCreation); - } - - await languageServer.WaitForShutdown().ConfigureAwait(false); - - // Resubscribe host logger to log shutdown events to the console - _logger.Subscribe(new PSHostLogger(_config.PSHost.UI)); } - private async Task RunTempDebugSession(HostStartupInfo hostDetails) + private async Task RunTempDebugSessionAsync(HostStartupInfo hostDetails) { _logger.Log(PsesLogLevel.Diagnostic, "Running temp debug session"); - PsesDebugServer debugServer = await CreateDebugServerForTempSession(hostDetails).ConfigureAwait(false); + PsesDebugServer debugServer = await CreateDebugServerForTempSessionAsync(hostDetails).ConfigureAwait(false); _logger.Log(PsesLogLevel.Verbose, "Debug server created"); await debugServer.StartAsync().ConfigureAwait(false); _logger.Log(PsesLogLevel.Verbose, "Debug server started"); @@ -173,32 +158,32 @@ private async Task StartDebugServer(Task debugServerCreation) return; } - private Task RestartDebugServer(PsesDebugServer debugServer, bool usePSReadLine) + private Task RestartDebugServerAsync(PsesDebugServer debugServer, bool usePSReadLine) { _logger.Log(PsesLogLevel.Diagnostic, "Restarting debug server"); - Task debugServerCreation = RecreateDebugServer(debugServer, usePSReadLine); + Task debugServerCreation = RecreateDebugServerAsync(debugServer, usePSReadLine); return StartDebugServer(debugServerCreation); } - private async Task CreateLanguageServer(HostStartupInfo hostDetails) + private async Task CreateLanguageServerAsync(HostStartupInfo hostDetails) { - _logger.Log(PsesLogLevel.Verbose, $"Creating LSP transport with endpoint {_config.LanguageServiceTransport.Endpoint}"); + _logger.Log(PsesLogLevel.Verbose, $"Creating LSP transport with endpoint {_config.LanguageServiceTransport.EndpointDetails}"); (Stream inStream, Stream outStream) = await _config.LanguageServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); _logger.Log(PsesLogLevel.Diagnostic, "Creating language server"); return _serverFactory.CreateLanguageServer(inStream, outStream, hostDetails); } - private async Task CreateDebugServerWithLanguageServer(PsesLanguageServer languageServer, bool usePSReadLine) + private async Task CreateDebugServerWithLanguageServerAsync(PsesLanguageServer languageServer, bool usePSReadLine) { - _logger.Log(PsesLogLevel.Verbose, $"Creating debug adapter transport with endpoint {_config.DebugServiceTransport.Endpoint}"); + _logger.Log(PsesLogLevel.Verbose, $"Creating debug adapter transport with endpoint {_config.DebugServiceTransport.EndpointDetails}"); (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); _logger.Log(PsesLogLevel.Diagnostic, "Creating debug adapter"); return _serverFactory.CreateDebugServerWithLanguageServer(inStream, outStream, languageServer, usePSReadLine); } - private async Task RecreateDebugServer(PsesDebugServer debugServer, bool usePSReadLine) + private async Task RecreateDebugServerAsync(PsesDebugServer debugServer, bool usePSReadLine) { _logger.Log(PsesLogLevel.Diagnostic, "Recreating debug adapter transport"); (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); @@ -207,7 +192,7 @@ private async Task RecreateDebugServer(PsesDebugServer debugSer return _serverFactory.RecreateDebugServer(inStream, outStream, debugServer, usePSReadLine); } - private async Task CreateDebugServerForTempSession(HostStartupInfo hostDetails) + private async Task CreateDebugServerForTempSessionAsync(HostStartupInfo hostDetails) { (Stream inStream, Stream outStream) = await _config.DebugServiceTransport.ConnectStreamsAsync().ConfigureAwait(false); @@ -245,6 +230,29 @@ private HostStartupInfo CreateHostStartupInfo() usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine); } + private void WriteStartupBanner() + { + if (_config.ConsoleRepl == ConsoleReplKind.None) + { + return; + } + + _config.PSHost.UI.WriteLine(@" + +__/\\\\\\\\\\\\\_______/\\\\\\\\\\\____/\\\\\\\\\\\________/\\\\\\\\\_ + _\/\\\/////////\\\___/\\\/////////\\\_\/////\\\///______/\\\////////__ + _\/\\\_______\/\\\__\//\\\______\///______\/\\\_______/\\\/___________ + _\/\\\\\\\\\\\\\/____\////\\\_____________\/\\\______/\\\_____________ + _\/\\\/////////_________\////\\\__________\/\\\_____\/\\\_____________ + _\/\\\_____________________\////\\\_______\/\\\_____\//\\\____________ + _\/\\\______________/\\\______\//\\\______\/\\\______\///\\\__________ + _\/\\\_____________\///\\\\\\\\\\\/____/\\\\\\\\\\\____\////\\\\\\\\\_ + _\///________________\///////////_____\///////////________\/////////__ + + ====== PowerShell Integrated Console ======= +"); + } + private void DebugServer_OnSessionEnded(object sender, EventArgs args) { _logger.Log(PsesLogLevel.Verbose, "Debug session ended. Restarting debug service"); @@ -253,7 +261,7 @@ private void DebugServer_OnSessionEnded(object sender, EventArgs args) _alreadySubscribedDebug = false; Task.Run(() => { - RestartDebugServer(oldServer, usePSReadLine: _config.ConsoleRepl == ConsoleReplKind.PSReadLine); + RestartDebugServerAsync(oldServer, usePSReadLine: _config.ConsoleRepl == ConsoleReplKind.PSReadLine); }); } } diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs index 6aeae5658..a7d6396e0 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs @@ -219,14 +219,13 @@ protected override void EndProcessing() { _logger.LogException("Exception encountered starting EditorServices", e); - // Give the user a chance to read the message + // Give the user a chance to read the message if they have a console if (!Stdio) { Host.UI.WriteLine("\n== Press any key to close terminal =="); + Console.ReadKey(); } - Console.ReadKey(); - ThrowTerminatingError(new ErrorRecord(e, "PowerShellEditorServicesError", ErrorCategory.NotSpecified, this)); } finally diff --git a/src/PowerShellEditorServices.Hosting/TransportConfig.cs b/src/PowerShellEditorServices.Hosting/TransportConfig.cs index 19fcad13f..072f2dc6d 100644 --- a/src/PowerShellEditorServices.Hosting/TransportConfig.cs +++ b/src/PowerShellEditorServices.Hosting/TransportConfig.cs @@ -25,7 +25,7 @@ public interface ITransportConfig /// /// The name of the transport endpoint for logging. /// - string Endpoint { get; } + string EndpointDetails { get; } /// /// The name of the transport to record in the session file. @@ -43,7 +43,7 @@ public interface ITransportConfig /// public class StdioTransportConfig : ITransportConfig { - public string Endpoint => ""; + public string EndpointDetails => ""; public string SessionFileTransportName => "Stdio"; @@ -91,7 +91,7 @@ private DuplexNamedPipeTransportConfig(string pipeName) SessionFileEntries = new Dictionary{ { "PipeName", NamedPipeUtils.GetNamedPipePath(pipeName) } }; } - public string Endpoint => $"InOut pipe: {_pipeName}"; + public string EndpointDetails => $"InOut pipe: {_pipeName}"; public string SessionFileTransportName => "NamedPipe"; @@ -110,11 +110,8 @@ private DuplexNamedPipeTransportConfig(string pipeName) /// public class SimplexNamedPipeTransportConfig : ITransportConfig { - private static readonly IReadOnlyList s_pipeNamePrefixes = new[] - { - "in", - "out", - }; + private const string InPipePrefix = "in"; + private const string OutPipePrefix = "out"; /// /// Create a pair of simplex named pipes using generated names. @@ -122,7 +119,7 @@ public class SimplexNamedPipeTransportConfig : ITransportConfig /// A new simplex named pipe transport config. public static SimplexNamedPipeTransportConfig Create() { - return SimplexNamedPipeTransportConfig.Create(NamedPipeUtils.GenerateValidNamedPipeName(s_pipeNamePrefixes)); + return SimplexNamedPipeTransportConfig.Create(NamedPipeUtils.GenerateValidNamedPipeName(new[] { InPipePrefix, OutPipePrefix })); } /// @@ -136,8 +133,8 @@ public static SimplexNamedPipeTransportConfig Create(string pipeNameBase) return SimplexNamedPipeTransportConfig.Create(); } - string inPipeName = $"{s_pipeNamePrefixes[0]}_{pipeNameBase}"; - string outPipeName = $"{s_pipeNamePrefixes[1]}_{pipeNameBase}"; + string inPipeName = $"{InPipePrefix}_{pipeNameBase}"; + string outPipeName = $"{OutPipePrefix}_{pipeNameBase}"; return SimplexNamedPipeTransportConfig.Create(inPipeName, outPipeName); } @@ -166,7 +163,7 @@ private SimplexNamedPipeTransportConfig(string inPipeName, string outPipeName) }; } - public string Endpoint => $"In pipe: {_inPipeName} Out pipe: {_outPipeName}"; + public string EndpointDetails => $"In pipe: {_inPipeName} Out pipe: {_outPipeName}"; public string SessionFileTransportName => "NamedPipeSimplex"; diff --git a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs index 710891a48..af783abf6 100644 --- a/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs +++ b/src/PowerShellEditorServices/Hosting/EditorServicesServerFactory.cs @@ -73,14 +73,14 @@ private EditorServicesServerFactory(ILoggerFactory loggerFactory, LogLevel minim /// /// The protocol transport input stream. /// The protocol transport output stream. - /// The host details configuration for Editor Services instantation. + /// The host details configuration for Editor Services instantation. /// A new, unstarted language server instance. public PsesLanguageServer CreateLanguageServer( Stream inputStream, Stream outputStream, - HostStartupInfo hostDetails) + HostStartupInfo hostStartupInfo) { - return new PsesLanguageServer(_loggerFactory, inputStream, outputStream, hostDetails); + return new PsesLanguageServer(_loggerFactory, inputStream, outputStream, hostStartupInfo); } /// diff --git a/src/PowerShellEditorServices/Server/PsesDebugServer.cs b/src/PowerShellEditorServices/Server/PsesDebugServer.cs index 25614b866..dd2b2f644 100644 --- a/src/PowerShellEditorServices/Server/PsesDebugServer.cs +++ b/src/PowerShellEditorServices/Server/PsesDebugServer.cs @@ -21,6 +21,8 @@ namespace Microsoft.PowerShell.EditorServices.Server /// internal class PsesDebugServer : IDisposable { + private static bool s_hasRunPsrlStaticCtor = false; + private readonly Stream _inputStream; private readonly Stream _outputStream; private readonly bool _useTempSession; @@ -70,8 +72,9 @@ public async Task StartAsync() _powerShellContextService.IsDebugServerActive = true; // Needed to make sure PSReadLine's static properties are initialized in the pipeline thread. - if (_usePSReadLine) + if (!s_hasRunPsrlStaticCtor && _usePSReadLine) { + s_hasRunPsrlStaticCtor = true; _powerShellContextService .ExecuteScriptStringAsync("[System.Runtime.CompilerServices.RuntimeHelpers]::RunClassConstructor([Microsoft.PowerShell.PSConsoleReadLine].TypeHandle)") .Wait(); diff --git a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs index c0cfa8d8a..fbb975b6d 100644 --- a/src/PowerShellEditorServices/Server/PsesLanguageServer.cs +++ b/src/PowerShellEditorServices/Server/PsesLanguageServer.cs @@ -36,18 +36,18 @@ internal class PsesLanguageServer /// Factory to create loggers with. /// Protocol transport input stream. /// Protocol transport output stream. - /// Host configuration to instantiate the server and services with. + /// Host configuration to instantiate the server and services with. public PsesLanguageServer( ILoggerFactory factory, Stream inputStream, Stream outputStream, - HostStartupInfo hostDetails) + HostStartupInfo hostStartupInfo) { LoggerFactory = factory; - _minimumLogLevel = (LogLevel)hostDetails.LogLevel; + _minimumLogLevel = (LogLevel)hostStartupInfo.LogLevel; _inputStream = inputStream; _outputStream = outputStream; - _hostDetails = hostDetails; + _hostDetails = hostStartupInfo; _serverStart = new TaskCompletionSource(); } @@ -66,8 +66,8 @@ public async Task StartAsync() .AddPsesLanguageServices(_hostDetails)) .ConfigureLogging(builder => builder .AddSerilog(Log.Logger) - .AddLanguageServer(LogLevel.Trace) - .SetMinimumLevel(LogLevel.Trace)) + .AddLanguageServer(_minimumLogLevel) + .SetMinimumLevel(_minimumLogLevel)) .WithHandler() .WithHandler() .WithHandler() diff --git a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs index 150e50c25..7ad24f36b 100644 --- a/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs +++ b/src/PowerShellEditorServices/Server/PsesServiceCollectionExtensions.cs @@ -17,7 +17,7 @@ internal static class PsesServiceCollectionExtensions { public static IServiceCollection AddPsesLanguageServices( this IServiceCollection collection, - HostStartupInfo hostDetails) + HostStartupInfo hostStartupInfo) { return collection.AddSingleton() .AddSingleton() @@ -27,7 +27,7 @@ public static IServiceCollection AddPsesLanguageServices( PowerShellContextService.Create( provider.GetService(), provider.GetService(), - hostDetails)) + hostStartupInfo)) .AddSingleton() .AddSingleton() .AddSingleton() diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index 52b7e509d..037b8427f 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -173,7 +173,7 @@ public PowerShellContextService( public static PowerShellContextService Create( ILoggerFactory factory, OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServer languageServer, - HostStartupInfo hostDetails) + HostStartupInfo hostStartupInfo) { var logger = factory.CreateLogger(); @@ -182,8 +182,8 @@ public static PowerShellContextService Create( // We also want it if we are either: // * On Windows on any version OR // * On Linux or macOS on any version greater than or equal to 7 - bool shouldUsePSReadLine = hostDetails.ConsoleReplEnabled - && !hostDetails.UsesLegacyReadLine + bool shouldUsePSReadLine = hostStartupInfo.ConsoleReplEnabled + && !hostStartupInfo.UsesLegacyReadLine && (VersionUtils.IsWindows || !VersionUtils.IsPS6); var powerShellContext = new PowerShellContextService( @@ -192,25 +192,25 @@ public static PowerShellContextService Create( shouldUsePSReadLine); EditorServicesPSHostUserInterface hostUserInterface = - hostDetails.ConsoleReplEnabled - ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, hostDetails.PSHost) + hostStartupInfo.ConsoleReplEnabled + ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, logger, hostStartupInfo.PSHost) : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); EditorServicesPSHost psHost = new EditorServicesPSHost( powerShellContext, - hostDetails, + hostStartupInfo, hostUserInterface, logger); Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost); - powerShellContext.Initialize(hostDetails.ProfilePaths, initialRunspace, true, hostUserInterface); + powerShellContext.Initialize(hostStartupInfo.ProfilePaths, initialRunspace, true, hostUserInterface); powerShellContext.ImportCommandsModuleAsync(); // 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 hostDetails.AdditionalModules) + foreach (string module in hostStartupInfo.AdditionalModules) { var command = new PSCommand() From ff2b058cd5d5f3019c0f2491c08ee4207e79486a Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 19:39:03 -0800 Subject: [PATCH 48/62] Improve startup logging --- .../EditorServicesLoader.cs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 6a71ffb9f..e8dc26240 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -165,20 +165,19 @@ private EditorServicesLoader( /// public async Task LoadAndRunEditorServicesAsync() { + // Log important host information here + LogHostInformation(); + #if !CoreCLR // Make sure the .NET Framework version supports .NET Standard 2.0 CheckNetFxVersion(); #endif - // Ensure the language mode allows us to run CheckLanguageMode(); // Add the bundled modules to the PSModulePath UpdatePSModulePath(); - // Log important host information here - LogHostInformation(); - // Check to see if the configuration we have is valid ValidateConfiguration(); @@ -213,6 +212,8 @@ private void CheckNetFxVersion() return; } + _logger.Log(PsesLogLevel.Verbose, $".NET registry version: {netFxVersion}"); + if (netFxVersion < Net461Version) { _logger.Log(PsesLogLevel.Warning, $".NET Framework version {netFxVersion} lower than .NET 4.6.1. This runtime is not supported and you may experience errors. Please update your .NET runtime version."); @@ -285,10 +286,7 @@ private void LogHostInformation() - PowerShell output encoding: {GetPSOutputEncoding()} "); - _logger.Log(PsesLogLevel.Verbose, $@" -== PowerShell Details == - - PowerShell version: {GetPowerShellVersion()} -"); + LogPowerShellDetails(); LogOperatingSystemDetails(); } @@ -301,11 +299,17 @@ private string GetPSOutputEncoding() } } - private string GetPowerShellVersion() + private void LogPowerShellDetails() { - using (var pwsh = SMA.PowerShell.Create()) + using (var pwsh = SMA.PowerShell.Create(SMA.RunspaceMode.CurrentRunspace)) { - return pwsh.AddScript("$PSVersionTable.PSVersion").Invoke()[0].ToString(); + string psVersion = pwsh.AddScript("$PSVersionTable.PSVersion").Invoke()[0].ToString(); + + _logger.Log(PsesLogLevel.Verbose, $@" +== PowerShell Details == +- PowerShell version: {psVersion} +- Language mode: {pwsh.Runspace.SessionStateProxy.LanguageMode} +"); } } From 9ea332b4d393109d0d1aafb2823f60a253726708 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 19:42:34 -0800 Subject: [PATCH 49/62] Load PSES at right time --- .../EditorServicesLoader.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index e8dc26240..055ef6cc1 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -182,8 +182,8 @@ public async Task LoadAndRunEditorServicesAsync() ValidateConfiguration(); // Method with no implementation that forces the PSES assembly to load, triggering an AssemblyResolve event - _logger.Log(PsesLogLevel.Verbose, "Loading PSES assemblies"); - EditorServicesLoading.LoadEditorServicesForHost(); + _logger.Log(PsesLogLevel.Verbose, "Loading PowerShell Editor Services"); + LoadEditorServices(); _logger.Log(PsesLogLevel.Verbose, "Starting EditorServices"); using (var editorServicesRunner = new EditorServicesRunner(_logger, _hostConfig, _sessionFileWriter, _loggersToUnsubscribe)) @@ -200,6 +200,11 @@ public void Dispose() // This is not high priority, since the PSES process shouldn't be reused } + private void LoadEditorServices() + { + EditorServicesLoading.LoadEditorServicesForHost(); + } + #if !CoreCLR private void CheckNetFxVersion() { From 6b0ee5be6e24bcefe4f797d4e3a4644ba99f4968 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 21:01:30 -0800 Subject: [PATCH 50/62] Fix PSModulePath in setup --- src/PowerShellEditorServices.Hosting/SessionFileWriter.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs index 16696e336..12842d618 100644 --- a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs +++ b/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.Collections.Generic; using System.IO; using System.Management.Automation; @@ -127,6 +128,7 @@ public void WriteSessionStarted(ITransportConfig languageServiceTransport, ITran /// The dictionary representing the session file. private void WriteSessionObject(Dictionary sessionObject) { + string psModulePath = Environment.GetEnvironmentVariable("PSModulePath"); string content = null; using (var pwsh = SMA.PowerShell.Create(RunspaceMode.NewRunspace)) { @@ -136,6 +138,10 @@ private void WriteSessionObject(Dictionary sessionObject) .AddParameter("Compress") .Invoke()[0]; + // Runspace creation has a bug where it resets the PSModulePath, + // which we must correct for + Environment.SetEnvironmentVariable("PSModulePath", psModulePath); + File.WriteAllText(_sessionFilePath, content, s_sessionFileEncoding); } From f9f46430ff029b025d08d7a28054760fcace952c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 21:04:37 -0800 Subject: [PATCH 51/62] Increase startup banner spacing --- src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index 2a17e0cf3..a21a25a6b 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -239,6 +239,7 @@ private void WriteStartupBanner() _config.PSHost.UI.WriteLine(@" + __/\\\\\\\\\\\\\_______/\\\\\\\\\\\____/\\\\\\\\\\\________/\\\\\\\\\_ _\/\\\/////////\\\___/\\\/////////\\\_\/////\\\///______/\\\////////__ _\/\\\_______\/\\\__\//\\\______\///______\/\\\_______/\\\/___________ @@ -249,7 +250,10 @@ private void WriteStartupBanner() _\/\\\_____________\///\\\\\\\\\\\/____/\\\\\\\\\\\____\////\\\\\\\\\_ _\///________________\///////////_____\///////////________\/////////__ + + ====== PowerShell Integrated Console ======= + "); } From 36bc03a3d73db486683134b4bdc24b1c1ba2215e Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 21:47:52 -0800 Subject: [PATCH 52/62] Move BuildInfo --- .gitignore | 2 +- PowerShellEditorServices.build.ps1 | 6 +++--- src/PowerShellEditorServices.Hosting/BuildInfo.cs | 10 ++++++++++ .../EditorServicesLoader.cs | 10 +++++++++- 4 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 src/PowerShellEditorServices.Hosting/BuildInfo.cs diff --git a/.gitignore b/.gitignore index 8d18bc259..219315a5b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,7 @@ docs/metadata/ *.zip # Generated build info file -src/PowerShellEditorServices/Hosting/BuildInfo.cs +src/PowerShellEditorServices.Hosting/BuildInfo.cs # quickbuild.exe /VersionGeneratingLogs/ diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 51b4cc24c..f52cee212 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -20,7 +20,7 @@ param( $script:IsUnix = $PSVersionTable.PSEdition -and $PSVersionTable.PSEdition -eq "Core" -and !$IsWindows $script:RequiredSdkVersion = (Get-Content (Join-Path $PSScriptRoot 'global.json') | ConvertFrom-Json).sdk.version -$script:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices", "Hosting", "BuildInfo.cs") +$script:BuildInfoPath = [System.IO.Path]::Combine($PSScriptRoot, "src", "PowerShellEditorServices.Hosting", "BuildInfo.cs") $script:NetRuntime = @{ Core = 'netcoreapp2.1' @@ -190,8 +190,8 @@ namespace Microsoft.PowerShell.EditorServices.Hosting { public static class BuildInfo { - public const string BuildVersion = "$buildVersion"; - public const string BuildOrigin = "$buildOrigin"; + public static readonly string BuildVersion = "$buildVersion"; + public static readonly string BuildOrigin = "$buildOrigin"; public static readonly System.DateTime? BuildTime = System.DateTime.Parse("$buildTime"); } } diff --git a/src/PowerShellEditorServices.Hosting/BuildInfo.cs b/src/PowerShellEditorServices.Hosting/BuildInfo.cs new file mode 100644 index 000000000..2f45eb38c --- /dev/null +++ b/src/PowerShellEditorServices.Hosting/BuildInfo.cs @@ -0,0 +1,10 @@ +namespace Microsoft.PowerShell.EditorServices.Hosting +{ + public static class BuildInfo + { + public static readonly string BuildVersion = ""; + public static readonly string BuildOrigin = ""; + public static readonly System.DateTime? BuildTime = System.DateTime.Parse("2019-12-06T21:43:41"); + public static readonly bool IsPreview = false; + } +} diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 055ef6cc1..12be748c6 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -124,7 +124,7 @@ public static EditorServicesLoader Create( var asmName = new AssemblyName(args.Name); string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); - + if (!File.Exists(asmPath)) { return null; @@ -261,6 +261,14 @@ private void UpdatePSModulePath() private void LogHostInformation() { _logger.Log(PsesLogLevel.Diagnostic, "Logging host information"); + + _logger.Log(PsesLogLevel.Verbose, $@" +== Build Details == +- Editor Services version: {BuildInfo.BuildVersion} +- Build origin: {BuildInfo.BuildOrigin} +- Build time: {BuildInfo.BuildTime} +"); + _logger.Log(PsesLogLevel.Verbose, $@" == Host Startup Configuration Details == - Host name: {_hostConfig.HostInfo.Name} From 8d7f7cdd14f9d5bd27ac9af24b48652b03fc0202 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 21:58:26 -0800 Subject: [PATCH 53/62] Minor buildinfo change --- src/PowerShellEditorServices.Hosting/BuildInfo.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/PowerShellEditorServices.Hosting/BuildInfo.cs b/src/PowerShellEditorServices.Hosting/BuildInfo.cs index 2f45eb38c..386ac95a3 100644 --- a/src/PowerShellEditorServices.Hosting/BuildInfo.cs +++ b/src/PowerShellEditorServices.Hosting/BuildInfo.cs @@ -5,6 +5,5 @@ public static class BuildInfo public static readonly string BuildVersion = ""; public static readonly string BuildOrigin = ""; public static readonly System.DateTime? BuildTime = System.DateTime.Parse("2019-12-06T21:43:41"); - public static readonly bool IsPreview = false; } } From dd921d3a83cb51e2d9816f09e6c302f75715c32b Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 21:58:47 -0800 Subject: [PATCH 54/62] Add comment to .NET Framework dependency resolution --- src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index 12be748c6..ca2a61d97 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -117,14 +117,13 @@ public static EditorServicesLoader Create( }; } + // Unlike in .NET Core, we need to be look for all dependencies in .NET Framework, not just PSES.dll AppDomain.CurrentDomain.AssemblyResolve += (object sender, ResolveEventArgs args) => { logger.Log(PsesLogLevel.Diagnostic, $"Assembly resolve event fired for {args.Name}"); var asmName = new AssemblyName(args.Name); - string asmPath = Path.Combine(s_psesDependencyDirPath, $"{asmName.Name}.dll"); - if (!File.Exists(asmPath)) { return null; From ef720f14250dbe0396c01b16e5074c1994cc6ea1 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 22:00:15 -0800 Subject: [PATCH 55/62] Update BuildInfo unindex --- PowerShellEditorServices.build.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index f52cee212..bc56a46ac 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -35,7 +35,7 @@ $script:VSCodeOutput = "$PSScriptRoot/src/PowerShellEditorServices.VSCode/bin/$C if (Get-Command git -ErrorAction SilentlyContinue) { # ignore changes to this file - git update-index --assume-unchanged "$PSScriptRoot/src/PowerShellEditorServices.Host/BuildInfo/BuildInfo.cs" + git update-index --assume-unchanged "$PSScriptRoot/src/PowerShellEditorServices.Hosting/BuildInfo.cs" } function Invoke-WithCreateDefaultHook { From 2ab5db7491829812caa9b008482218a17bb37223 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 22:03:07 -0800 Subject: [PATCH 56/62] Small fixes --- .../EditorServicesLoader.cs | 2 +- .../EditorServicesRunner.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs index ca2a61d97..32919433d 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesLoader.cs @@ -45,7 +45,7 @@ public sealed class EditorServicesLoader : IDisposable public static EditorServicesLoader Create( HostLogger logger, EditorServicesConfig hostConfig, - ISessionFileWriter sessionFileWriter) => Create(logger, hostConfig, sessionFileWriter, null); + ISessionFileWriter sessionFileWriter) => Create(logger, hostConfig, sessionFileWriter, loggersToUnsubscribe: null); /// /// Create a new Editor Services loader. diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs index a21a25a6b..a36c78013 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs @@ -99,9 +99,12 @@ private async Task CreateEditorServicesAndRunUntilShutdown() // Unsubscribe the host logger here so that the integrated console is not polluted with input after the first prompt _logger.Log(PsesLogLevel.Verbose, "Starting server, deregistering host logger and registering shutdown listener"); - foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) + if (_loggersToUnsubscribe != null) { - loggerToUnsubscribe.Dispose(); + foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) + { + loggerToUnsubscribe.Dispose(); + } } WriteStartupBanner(); From f20d1affb8ce0b4785336e604f118316cab0b6b8 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 22:24:11 -0800 Subject: [PATCH 57/62] Move files into nice dirs --- .../StartEditorServicesCommand.cs | 5 +++-- .../EditorServicesConfig.cs | 0 .../{ => Configuration}/HostInfo.cs | 0 .../{ => Configuration}/HostLogger.cs | 0 .../{ => Configuration}/SessionFileWriter.cs | 0 .../{ => Configuration}/TransportConfig.cs | 0 .../{ => Internal}/EditorServicesRunner.cs | 18 ++++++++---------- .../{ => Internal}/NamedPipeUtils.cs | 0 .../{ => Internal}/PsesLoadContext.cs | 0 .../PowerShellEditorServices.Hosting.csproj | 2 +- 10 files changed, 12 insertions(+), 13 deletions(-) rename src/PowerShellEditorServices.Hosting/{ => Commands}/StartEditorServicesCommand.cs (99%) rename src/PowerShellEditorServices.Hosting/{ => Configuration}/EditorServicesConfig.cs (100%) rename src/PowerShellEditorServices.Hosting/{ => Configuration}/HostInfo.cs (100%) rename src/PowerShellEditorServices.Hosting/{ => Configuration}/HostLogger.cs (100%) rename src/PowerShellEditorServices.Hosting/{ => Configuration}/SessionFileWriter.cs (100%) rename src/PowerShellEditorServices.Hosting/{ => Configuration}/TransportConfig.cs (100%) rename src/PowerShellEditorServices.Hosting/{ => Internal}/EditorServicesRunner.cs (98%) rename src/PowerShellEditorServices.Hosting/{ => Internal}/NamedPipeUtils.cs (100%) rename src/PowerShellEditorServices.Hosting/{ => Internal}/PsesLoadContext.cs (100%) diff --git a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs similarity index 99% rename from src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs rename to src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs index a7d6396e0..e328308ba 100644 --- a/src/PowerShellEditorServices.Hosting/StartEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/Commands/StartEditorServicesCommand.cs @@ -11,6 +11,7 @@ using System.Management.Automation; using System.Reflection; using SMA = System.Management.Automation; +using Microsoft.PowerShell.EditorServices.Hosting; #if DEBUG using System.Diagnostics; @@ -19,7 +20,7 @@ using Debugger = System.Diagnostics.Debugger; #endif -namespace Microsoft.PowerShell.EditorServices.Hosting +namespace Microsoft.PowerShell.EditorServices.Commands { /// /// The Start-EditorServices command, the conventional entrypoint for PowerShell Editor Services. @@ -30,7 +31,7 @@ public sealed class StartEditorServicesCommand : PSCmdlet private readonly List _disposableResources; private readonly List _loggerUnsubscribers; - + private HostLogger _logger; public StartEditorServicesCommand() diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs b/src/PowerShellEditorServices.Hosting/Configuration/EditorServicesConfig.cs similarity index 100% rename from src/PowerShellEditorServices.Hosting/EditorServicesConfig.cs rename to src/PowerShellEditorServices.Hosting/Configuration/EditorServicesConfig.cs diff --git a/src/PowerShellEditorServices.Hosting/HostInfo.cs b/src/PowerShellEditorServices.Hosting/Configuration/HostInfo.cs similarity index 100% rename from src/PowerShellEditorServices.Hosting/HostInfo.cs rename to src/PowerShellEditorServices.Hosting/Configuration/HostInfo.cs diff --git a/src/PowerShellEditorServices.Hosting/HostLogger.cs b/src/PowerShellEditorServices.Hosting/Configuration/HostLogger.cs similarity index 100% rename from src/PowerShellEditorServices.Hosting/HostLogger.cs rename to src/PowerShellEditorServices.Hosting/Configuration/HostLogger.cs diff --git a/src/PowerShellEditorServices.Hosting/SessionFileWriter.cs b/src/PowerShellEditorServices.Hosting/Configuration/SessionFileWriter.cs similarity index 100% rename from src/PowerShellEditorServices.Hosting/SessionFileWriter.cs rename to src/PowerShellEditorServices.Hosting/Configuration/SessionFileWriter.cs diff --git a/src/PowerShellEditorServices.Hosting/TransportConfig.cs b/src/PowerShellEditorServices.Hosting/Configuration/TransportConfig.cs similarity index 100% rename from src/PowerShellEditorServices.Hosting/TransportConfig.cs rename to src/PowerShellEditorServices.Hosting/Configuration/TransportConfig.cs diff --git a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs similarity index 98% rename from src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs rename to src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs index a36c78013..8d398b329 100644 --- a/src/PowerShellEditorServices.Hosting/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs @@ -6,10 +6,8 @@ using Microsoft.PowerShell.EditorServices.Server; using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.IO; using System.Threading.Tasks; -using SMA = System.Management.Automation; namespace Microsoft.PowerShell.EditorServices.Hosting { @@ -243,14 +241,14 @@ private void WriteStartupBanner() _config.PSHost.UI.WriteLine(@" -__/\\\\\\\\\\\\\_______/\\\\\\\\\\\____/\\\\\\\\\\\________/\\\\\\\\\_ - _\/\\\/////////\\\___/\\\/////////\\\_\/////\\\///______/\\\////////__ - _\/\\\_______\/\\\__\//\\\______\///______\/\\\_______/\\\/___________ - _\/\\\\\\\\\\\\\/____\////\\\_____________\/\\\______/\\\_____________ - _\/\\\/////////_________\////\\\__________\/\\\_____\/\\\_____________ - _\/\\\_____________________\////\\\_______\/\\\_____\//\\\____________ - _\/\\\______________/\\\______\//\\\______\/\\\______\///\\\__________ - _\/\\\_____________\///\\\\\\\\\\\/____/\\\\\\\\\\\____\////\\\\\\\\\_ +__/\\\\\\\\\\\\\_______/\\\\\\\\\\\____/\\\\\\\\\\\________/\\\\\\\\\_ + _\/\\\/////////\\\___/\\\/////////\\\_\/////\\\///______/\\\////////__ + _\/\\\_______\/\\\__\//\\\______\///______\/\\\_______/\\\/___________ + _\/\\\\\\\\\\\\\/____\////\\\_____________\/\\\______/\\\_____________ + _\/\\\/////////_________\////\\\__________\/\\\_____\/\\\_____________ + _\/\\\_____________________\////\\\_______\/\\\_____\//\\\____________ + _\/\\\______________/\\\______\//\\\______\/\\\______\///\\\__________ + _\/\\\_____________\///\\\\\\\\\\\/____/\\\\\\\\\\\____\////\\\\\\\\\_ _\///________________\///////////_____\///////////________\/////////__ diff --git a/src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs b/src/PowerShellEditorServices.Hosting/Internal/NamedPipeUtils.cs similarity index 100% rename from src/PowerShellEditorServices.Hosting/NamedPipeUtils.cs rename to src/PowerShellEditorServices.Hosting/Internal/NamedPipeUtils.cs diff --git a/src/PowerShellEditorServices.Hosting/PsesLoadContext.cs b/src/PowerShellEditorServices.Hosting/Internal/PsesLoadContext.cs similarity index 100% rename from src/PowerShellEditorServices.Hosting/PsesLoadContext.cs rename to src/PowerShellEditorServices.Hosting/Internal/PsesLoadContext.cs diff --git a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj index 6f6676e95..80759709e 100644 --- a/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj +++ b/src/PowerShellEditorServices.Hosting/PowerShellEditorServices.Hosting.csproj @@ -33,7 +33,7 @@ - + From edf7bbd095cf3d1de40f1003c8a41bd61d791a8a Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Fri, 6 Dec 2019 23:15:41 -0800 Subject: [PATCH 58/62] Fix tests --- .../TestsFixture.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs b/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs index 7b51226f3..e56d4668f 100644 --- a/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs +++ b/test/PowerShellEditorServices.Test.E2E/TestsFixture.cs @@ -58,16 +58,17 @@ public async Task InitializeAsync() List args = new List { - Path.Combine(s_bundledModulePath, "PowerShellEditorServices", "Start-EditorServices.ps1"), - "-LogPath", s_logPath, + "&", + SingleQuoteEscape(Path.Combine(s_bundledModulePath, "PowerShellEditorServices", "Start-EditorServices.ps1")), + "-LogPath", SingleQuoteEscape(s_logPath), "-LogLevel", s_logLevel, - "-SessionDetailsPath", s_sessionDetailsPath, + "-SessionDetailsPath", SingleQuoteEscape(s_sessionDetailsPath), "-FeatureFlags", string.Join(',', s_featureFlags), "-HostName", s_hostName, "-HostProfileId", s_hostProfileId, "-HostVersion", s_hostVersion, "-AdditionalModules", string.Join(',', s_additionalModules), - "-BundledModulesPath", s_bundledModulePath, + "-BundledModulesPath", SingleQuoteEscape(s_bundledModulePath), "-Stdio" }; @@ -95,5 +96,10 @@ public virtual async Task DisposeAsync() public abstract Task CustomInitializeAsync( ILoggerFactory factory, StdioServerProcess process); + + private static string SingleQuoteEscape(string str) + { + return $"'{str.Replace("'", "''")}'"; + } } } From 69b9583a0b37dccfbe21a6e97e460c01d6511612 Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 9 Dec 2019 13:46:33 -0800 Subject: [PATCH 59/62] Update PowerShellEditorServices.build.ps1 Co-Authored-By: Tyler James Leonhardt --- PowerShellEditorServices.build.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index bc56a46ac..b163f320d 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -213,6 +213,8 @@ task Build { { exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Hosting\PowerShellEditorServices.Hosting.csproj -f $script:NetRuntime.Desktop } } + + # Build PowerShellEditorServices.VSCode module exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj -f $script:NetRuntime.Standard } } From 7951fb3df0ab8eefeb8fe10b605609e7e3e92f2c Mon Sep 17 00:00:00 2001 From: Robert Holt Date: Mon, 9 Dec 2019 13:46:44 -0800 Subject: [PATCH 60/62] Update src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs Co-Authored-By: Tyler James Leonhardt --- .../Internal/EditorServicesRunner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs index 8d398b329..236ea638a 100644 --- a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs @@ -253,7 +253,7 @@ private void WriteStartupBanner() - ====== PowerShell Integrated Console ======= + ====== PowerShell Integrated Console ====== "); } From dde7ea756de34d183497786840f47244f6cb798c Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Mon, 9 Dec 2019 14:10:15 -0800 Subject: [PATCH 61/62] Skip adding SMA asms to module --- PowerShellEditorServices.build.ps1 | 11 ++++++++++- .../Internal/EditorServicesRunner.cs | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index b163f320d..c7431abd1 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -213,7 +213,7 @@ task Build { { exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.Hosting\PowerShellEditorServices.Hosting.csproj -f $script:NetRuntime.Desktop } } - + # Build PowerShellEditorServices.VSCode module exec { & $script:dotnetExe publish -c $Configuration .\src\PowerShellEditorServices.VSCode\PowerShellEditorServices.VSCode.csproj -f $script:NetRuntime.Standard } } @@ -290,7 +290,12 @@ task LayoutModule -After Build { Copy-Item -Path "$script:PsesOutput/runtimes/osx-64/native/*" -Destination $psesDepsPath Copy-Item -Path "$script:PsesOutput/runtimes/linux-64/native/*" -Destination $psesDepsPath + # Assemble PSES module + $includedDlls = [System.Collections.Generic.HashSet[string]]::new() + [void]$includedDlls.Add('System.Management.Automation.dll') + + # PSES/bin/Common foreach ($psesComponent in Get-ChildItem $script:PsesOutput) { if ($psesComponent.Name -eq 'System.Management.Automation.dll' -or @@ -306,6 +311,7 @@ task LayoutModule -After Build { } } + # PSES/bin/Core foreach ($hostComponent in Get-ChildItem $script:HostCoreOutput) { if (-not $includedDlls.Contains($hostComponent.Name)) @@ -314,6 +320,7 @@ task LayoutModule -After Build { } } + # PSES/bin/Desktop if (-not $script:IsUnix) { foreach ($hostComponent in Get-ChildItem $script:HostDeskOutput) @@ -325,6 +332,8 @@ task LayoutModule -After Build { } } + # Assemble the PowerShellEditorServices.VSCode module + foreach ($vscodeComponent in Get-ChildItem $script:VSCodeOutput) { if (-not $includedDlls.Contains($vscodeComponent.Name)) diff --git a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs index 236ea638a..8a9e4316a 100644 --- a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs @@ -253,7 +253,7 @@ private void WriteStartupBanner() - ====== PowerShell Integrated Console ====== + =====> PowerShell Integrated Console <===== "); } From ddb97afc8661e6bda8e6d6d7b08715dbb9b6917f Mon Sep 17 00:00:00 2001 From: Rob Holt Date: Mon, 9 Dec 2019 14:17:01 -0800 Subject: [PATCH 62/62] Fix codacy bits --- .../Internal/EditorServicesRunner.cs | 1 - src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs index 8a9e4316a..73b869d5a 100644 --- a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs @@ -156,7 +156,6 @@ private async Task StartDebugServer(Task debugServerCreation) _logger.Log(PsesLogLevel.Diagnostic, "Starting debug server"); debugServer.StartAsync(); - return; } private Task RestartDebugServerAsync(PsesDebugServer debugServer, bool usePSReadLine) diff --git a/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs b/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs index 318b6dac2..6b65e5d9a 100644 --- a/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs +++ b/src/PowerShellEditorServices/Logging/HostLoggerAdapter.cs @@ -23,6 +23,7 @@ public HostLoggerAdapter(ILoggerFactory loggerFactory) public void OnCompleted() { + // Nothing to do; we simply don't send more log messages } public void OnError(Exception error)