diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 60274440e..cd2d35151 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -134,7 +134,7 @@ task Clean BinClean,{ exec { & $script:dotnetExe clean } Get-ChildItem -Recurse $PSScriptRoot\src\*.nupkg | Remove-Item -Force -ErrorAction Ignore Get-ChildItem $PSScriptRoot\PowerShellEditorServices*.zip | Remove-Item -Force -ErrorAction Ignore - Get-ChildItem $PSScriptRoot\module\PowerShellEditorServices\Commands\en-US\*-help.xml | Remove-Item -Force -ErrorAction Ignore + Get-ChildItem $PSScriptRoot\module\PowerShellEditorServices.Commands\en-US\*-help.xml | Remove-Item -Force -ErrorAction Ignore # Remove bundled component modules $moduleJsonPath = "$PSScriptRoot\modules.json" @@ -406,7 +406,7 @@ task RestorePsesModules -After Build { } task BuildCmdletHelp { - New-ExternalHelp -Path $PSScriptRoot\module\docs -OutputPath $PSScriptRoot\module\PowerShellEditorServices\Commands\en-US -Force + New-ExternalHelp -Path $PSScriptRoot\module\docs -OutputPath $PSScriptRoot\module\PowerShellEditorServices.Commands\en-US -Force New-ExternalHelp -Path $PSScriptRoot\module\PowerShellEditorServices.VSCode\docs -OutputPath $PSScriptRoot\module\PowerShellEditorServices.VSCode\en-US -Force } diff --git a/module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.psd1 b/module/PowerShellEditorServices.Commands/PowerShellEditorServices.Commands.psd1 similarity index 100% rename from module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.psd1 rename to module/PowerShellEditorServices.Commands/PowerShellEditorServices.Commands.psd1 diff --git a/module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.psm1 b/module/PowerShellEditorServices.Commands/PowerShellEditorServices.Commands.psm1 similarity index 100% rename from module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.psm1 rename to module/PowerShellEditorServices.Commands/PowerShellEditorServices.Commands.psm1 diff --git a/module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.types.ps1xml b/module/PowerShellEditorServices.Commands/PowerShellEditorServices.Commands.types.ps1xml similarity index 99% rename from module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.types.ps1xml rename to module/PowerShellEditorServices.Commands/PowerShellEditorServices.Commands.types.ps1xml index eaebcb72a..acd4a67e4 100644 --- a/module/PowerShellEditorServices/Commands/PowerShellEditorServices.Commands.types.ps1xml +++ b/module/PowerShellEditorServices.Commands/PowerShellEditorServices.Commands.types.ps1xml @@ -25,4 +25,4 @@ - \ No newline at end of file + diff --git a/module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 b/module/PowerShellEditorServices.Commands/Private/BuiltInCommands.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Private/BuiltInCommands.ps1 rename to module/PowerShellEditorServices.Commands/Private/BuiltInCommands.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/Clear-Host.ps1 b/module/PowerShellEditorServices.Commands/Public/Clear-Host.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/Clear-Host.ps1 rename to module/PowerShellEditorServices.Commands/Public/Clear-Host.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 b/module/PowerShellEditorServices.Commands/Public/CmdletInterface.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/CmdletInterface.ps1 rename to module/PowerShellEditorServices.Commands/Public/CmdletInterface.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/ConvertFrom-ScriptExtent.ps1 b/module/PowerShellEditorServices.Commands/Public/ConvertFrom-ScriptExtent.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/ConvertFrom-ScriptExtent.ps1 rename to module/PowerShellEditorServices.Commands/Public/ConvertFrom-ScriptExtent.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/ConvertTo-ScriptExtent.ps1 b/module/PowerShellEditorServices.Commands/Public/ConvertTo-ScriptExtent.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/ConvertTo-ScriptExtent.ps1 rename to module/PowerShellEditorServices.Commands/Public/ConvertTo-ScriptExtent.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/Find-Ast.ps1 b/module/PowerShellEditorServices.Commands/Public/Find-Ast.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/Find-Ast.ps1 rename to module/PowerShellEditorServices.Commands/Public/Find-Ast.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/Get-Token.ps1 b/module/PowerShellEditorServices.Commands/Public/Get-Token.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/Get-Token.ps1 rename to module/PowerShellEditorServices.Commands/Public/Get-Token.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 b/module/PowerShellEditorServices.Commands/Public/Import-EditorCommand.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/Import-EditorCommand.ps1 rename to module/PowerShellEditorServices.Commands/Public/Import-EditorCommand.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/Join-ScriptExtent.ps1 b/module/PowerShellEditorServices.Commands/Public/Join-ScriptExtent.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/Join-ScriptExtent.ps1 rename to module/PowerShellEditorServices.Commands/Public/Join-ScriptExtent.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/Out-CurrentFile.ps1 b/module/PowerShellEditorServices.Commands/Public/Out-CurrentFile.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/Out-CurrentFile.ps1 rename to module/PowerShellEditorServices.Commands/Public/Out-CurrentFile.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/Set-ScriptExtent.ps1 b/module/PowerShellEditorServices.Commands/Public/Set-ScriptExtent.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/Set-ScriptExtent.ps1 rename to module/PowerShellEditorServices.Commands/Public/Set-ScriptExtent.ps1 diff --git a/module/PowerShellEditorServices/Commands/Public/Test-ScriptExtent.ps1 b/module/PowerShellEditorServices.Commands/Public/Test-ScriptExtent.ps1 similarity index 100% rename from module/PowerShellEditorServices/Commands/Public/Test-ScriptExtent.ps1 rename to module/PowerShellEditorServices.Commands/Public/Test-ScriptExtent.ps1 diff --git a/module/PowerShellEditorServices/Commands/en-US/Strings.psd1 b/module/PowerShellEditorServices.Commands/en-US/Strings.psd1 similarity index 100% rename from module/PowerShellEditorServices/Commands/en-US/Strings.psd1 rename to module/PowerShellEditorServices.Commands/en-US/Strings.psd1 diff --git a/module/PowerShellEditorServices/InvokePesterStub.ps1 b/module/PowerShellEditorServices/InvokePesterStub.ps1 old mode 100755 new mode 100644 diff --git a/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs b/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs index b0adf6fc8..710b436ba 100644 --- a/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs +++ b/src/PowerShellEditorServices.Hosting/Commands/InvokeReadLineForEditorServicesCommand.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Linq; using System.Management.Automation; using System.Management.Automation.Runspaces; using System.Reflection; @@ -21,12 +22,12 @@ private delegate string ReadLineInvoker( private static Lazy s_readLine = new Lazy(() => { - Type type = Type.GetType("Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2"); + var allAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + var assemblies = allAssemblies.FirstOrDefault(a => a.FullName.Contains("Microsoft.PowerShell.PSReadLine2")); + var type = assemblies?.ExportedTypes?.FirstOrDefault(a => a.FullName == "Microsoft.PowerShell.PSConsoleReadLine"); MethodInfo method = type?.GetMethod( "ReadLine", - new[] { typeof(Runspace), typeof(EngineIntrinsics), typeof(CancellationToken) }); - - // TODO: Handle method being null here. This shouldn't ever happen. + new [] { typeof(Runspace), typeof(EngineIntrinsics), typeof(CancellationToken) }); return (ReadLineInvoker)method.CreateDelegate(typeof(ReadLineInvoker)); }); diff --git a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs index a68842876..d36c01cc0 100644 --- a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs @@ -137,7 +137,7 @@ private async Task CreateEditorServicesAndRunUntilShutdown() HostStartupInfo hostStartupInfo = CreateHostStartupInfo(); // If we just want a temp debug session, run that and do nothing else - if (isTempDebugSession) + if(isTempDebugSession) { await RunTempDebugSessionAsync(hostStartupInfo).ConfigureAwait(false); return; @@ -153,9 +153,9 @@ 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"); - if (_loggersToUnsubscribe != null) + if(_loggersToUnsubscribe != null) { - foreach (IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) + foreach(IDisposable loggerToUnsubscribe in _loggersToUnsubscribe) { loggerToUnsubscribe.Dispose(); } @@ -166,7 +166,7 @@ private async Task CreateEditorServicesAndRunUntilShutdown() PsesLanguageServer languageServer = await CreateLanguageServerAsync(hostStartupInfo).ConfigureAwait(false); Task debugServerCreation = null; - if (creatingDebugServer) + if(creatingDebugServer) { debugServerCreation = CreateDebugServerWithLanguageServerAsync(languageServer, usePSReadLine: _config.ConsoleRepl == ConsoleReplKind.PSReadLine); } @@ -174,14 +174,14 @@ private async Task CreateEditorServicesAndRunUntilShutdown() Task languageServerStart = languageServer.StartAsync(); Task debugServerStart = null; - if (creatingDebugServer) + if(creatingDebugServer) { // We don't need to wait for this to start, since we instead wait for it to complete later debugServerStart = StartDebugServer(debugServerCreation); } await languageServerStart.ConfigureAwait(false); - if (debugServerStart != null) + if(debugServerStart != null) { await debugServerStart.ConfigureAwait(false); } @@ -210,7 +210,7 @@ private async Task StartDebugServer(Task debugServerCreation) // 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) + if(!_alreadySubscribedDebug) { _logger.Log(PsesLogLevel.Diagnostic, "Subscribing debug server for session ended event"); _alreadySubscribedDebug = true; @@ -268,7 +268,7 @@ private HostStartupInfo CreateHostStartupInfo() _logger.Log(PsesLogLevel.Diagnostic, "Creating startup info object"); ProfilePathInfo profilePaths = null; - if (_config.ProfilePaths.AllUsersAllHosts != null + if(_config.ProfilePaths.AllUsersAllHosts != null || _config.ProfilePaths.AllUsersCurrentHost != null || _config.ProfilePaths.CurrentUserAllHosts != null || _config.ProfilePaths.CurrentUserCurrentHost != null) @@ -298,7 +298,7 @@ private HostStartupInfo CreateHostStartupInfo() private void WriteStartupBanner() { - if (_config.ConsoleRepl == ConsoleReplKind.None) + if(_config.ConsoleRepl == ConsoleReplKind.None) { return; } diff --git a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs index ed6fb3e11..6d09f0883 100644 --- a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs +++ b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs @@ -92,8 +92,8 @@ public sealed class HostStartupInfo public string LogPath { get; } /// - /// The InitialSessionState will be inherited from the orginal PowerShell process. This will - /// be used when creating runspaces so that we honor the same InitialSessionState. + /// The initialSessionState will be inherited from the orginal PowerShell process. + /// This will be used when creating runspaces so that we honor the same initialSessionState including allowed modules, cmdlets and language mode. /// public InitialSessionState InitialSessionState { get; } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index a3f0ec080..774ee146a 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -23,14 +23,41 @@ namespace Microsoft.PowerShell.EditorServices.Services { + using Microsoft.PowerShell.Commands; using System.Management.Automation; + public static class InitialSessionStateExtensions + { + public static void AddCommandAndAliasToInitialSessionState(this InitialSessionState iss, string moduleQualifiedCmdletName) + { + var shortName = moduleQualifiedCmdletName.Split('\\').LastOrDefault(); + var existingShortCmdlet = iss.Commands.FirstOrDefault(a => string.Compare(a.Name, shortName, true) == 0); + var existingLongCmdlet = iss.Commands.FirstOrDefault(a => string.Compare(a.Name, moduleQualifiedCmdletName, true) == 0); + // Can't have both short and long defined as CmdletEntries. One has to be an alias. + if(existingShortCmdlet is not SessionStateCmdletEntry) + { + if(existingShortCmdlet is not null) + { + iss.Commands.Remove(shortName, existingShortCmdlet.GetType()); + } + iss.Commands.Add(new SessionStateCmdletEntry(shortName, typeof(T), null)); + } + if(existingLongCmdlet is not SessionStateAliasEntry) + { + if(existingLongCmdlet is not null) + { + iss.Commands.Remove(moduleQualifiedCmdletName, existingLongCmdlet.GetType()); + } + iss.Commands.Add(new SessionStateAliasEntry(moduleQualifiedCmdletName, shortName, null)); + } + } + } /// /// Manages the lifetime and usage of a PowerShell session. /// Handles nested PowerShell prompts and also manages execution of /// commands whether inside or outside of the debugger. /// - internal class PowerShellContextService : IHostSupportsInteractiveSession + internal class PowerShellContextService: IHostSupportsInteractiveSession { // This is a default that can be overriden at runtime by the user or tests. private static string s_bundledModulePath = Path.GetFullPath(Path.Combine( @@ -41,9 +68,11 @@ internal class PowerShellContextService : IHostSupportsInteractiveSession private static string s_commandsModulePath => Path.GetFullPath(Path.Combine( s_bundledModulePath, - "PowerShellEditorServices", - "Commands", - "PowerShellEditorServices.Commands.psd1")); + "PowerShellEditorServices.Commands")); + + private static string s_psReadLineModulePath => Path.GetFullPath(Path.Combine( + s_bundledModulePath, + "PSReadLine")); private static readonly Action s_runspaceApartmentStateSetter; private static readonly PropertyInfo s_writeStreamProperty; @@ -57,7 +86,7 @@ static PowerShellContextService() { MethodInfo setterInfo = typeof(Runspace).GetProperty("ApartmentState").GetSetMethod(); Delegate setter = Delegate.CreateDelegate(typeof(Action), firstArgument: null, method: setterInfo); - s_runspaceApartmentStateSetter = (Action)setter; + s_runspaceApartmentStateSetter = (Action) setter; } if (VersionUtils.IsPS7OrGreater) @@ -190,24 +219,17 @@ public PowerShellContextService( RunspaceChanged += PowerShellContext_RunspaceChangedAsync; ExecutionStatusChanged += PowerShellContext_ExecutionStatusChangedAsync; } - [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Checked by Validate call")] public static PowerShellContextService Create( ILoggerFactory factory, OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer, - HostStartupInfo hostStartupInfo) + HostStartupInfo hostStartupInfo + ) { var logger = factory.CreateLogger(); Validate.IsNotNull(nameof(hostStartupInfo), hostStartupInfo); - // Respect a user provided bundled module path. - if (Directory.Exists(hostStartupInfo.BundledModulePath)) - { - logger.LogTrace($"Using new bundled module path: {hostStartupInfo.BundledModulePath}"); - s_bundledModulePath = hostStartupInfo.BundledModulePath; - } - bool shouldUsePSReadLine = hostStartupInfo.ConsoleReplEnabled && !hostStartupInfo.UsesLegacyReadLine; @@ -218,7 +240,7 @@ public static PowerShellContextService Create( EditorServicesPSHostUserInterface hostUserInterface = hostStartupInfo.ConsoleReplEnabled - ? (EditorServicesPSHostUserInterface) new TerminalPSHostUserInterface(powerShellContext, hostStartupInfo.PSHost, logger) + ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(powerShellContext, hostStartupInfo.PSHost, logger) : new ProtocolPSHostUserInterface(languageServer, powerShellContext, logger); EditorServicesPSHost psHost = @@ -230,30 +252,12 @@ public static PowerShellContextService Create( logger.LogTrace("Creating initial PowerShell runspace"); Runspace initialRunspace = PowerShellContextService.CreateRunspace(psHost, hostStartupInfo.InitialSessionState); - 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 hostStartupInfo.AdditionalModules) - { - var command = - new PSCommand() - .AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", module); - -#pragma warning disable CS4014 - // This call queues the loading on the pipeline thread, so no need to await - powerShellContext.ExecuteCommandAsync( - command, - sendOutputToHost: false, - sendErrorToHost: true); -#pragma warning restore CS4014 - } + powerShellContext.Initialize(hostStartupInfo, languageServer, true); return powerShellContext; } - + [SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "Checked by Validate call")] + /// /// Only used in testing. Creates a Runspace given HostStartupInfo instead of a PSHost. /// @@ -295,9 +299,8 @@ public static Runspace CreateRunspace(PSHost psHost, InitialSessionState initial { s_runspaceApartmentStateSetter(runspace, ApartmentState.STA); } - - runspace.ThreadOptions = PSThreadOptions.ReuseThread; - runspace.Open(); + + runspace.ThreadOptions = PSThreadOptions.ReuseThread; return runspace; } @@ -311,25 +314,127 @@ public static Runspace CreateRunspace(PSHost psHost, InitialSessionState initial /// If true, the PowerShellContext owns this runspace. /// An IHostOutput implementation. Optional. public void Initialize( - ProfilePathInfo profilePaths, - Runspace initialRunspace, - bool ownsInitialRunspace, - IHostOutput consoleHost) + HostStartupInfo hostStartupInfo, + OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer, + bool ownsInitialRunspace + ) { + var modulesToImport = new List(); + // Respect a user provided bundled module path. + if (Directory.Exists(hostStartupInfo.BundledModulePath)) + { + logger.LogTrace($"Using new bundled module path: {hostStartupInfo.BundledModulePath}"); + s_bundledModulePath = hostStartupInfo.BundledModulePath; + } + + modulesToImport.Add(s_commandsModulePath); + if (this.isPSReadLineEnabled) + { + modulesToImport.Add(Path.Combine(s_bundledModulePath, "PSReadLine")); + } + if (hostStartupInfo.AdditionalModules is not null) + { + modulesToImport.AddRange(hostStartupInfo.AdditionalModules); + } + bool preloadModules = !hostStartupInfo.InitialSessionState.Providers.Any(a => a.Name == "FileSystem" && a.Visibility == SessionStateEntryVisibility.Public); + EditorServicesPSHostUserInterface hostUserInterface = + hostStartupInfo.ConsoleReplEnabled && hostStartupInfo.PSHost is not null + ? (EditorServicesPSHostUserInterface)new TerminalPSHostUserInterface(this, hostStartupInfo.PSHost, logger) + : new ProtocolPSHostUserInterface(languageServer, this, logger); + + EditorServicesPSHost psHost = + new EditorServicesPSHost( + this, + hostStartupInfo, + hostUserInterface, + logger); + Runspace initialRunspace; + if (preloadModules) + { + // Loading modules with ImportPSModulesFromPath into the InitialSessionState because in a Constrained Runspace there may not be a FileSystem provider. + // Import-Module provides the user with better errors, but may not be allowed within a constrained runspace + // ImportPSModule throws System.Management.Automation.DriveNotFoundException: 'Cannot find drive. A drive with the name 'C' does not exist.' + // ImportPSModulesFromPath loads the modules fine + foreach (var module in modulesToImport.Where(a => !string.IsNullOrEmpty(a))) + { + if (!File.Exists(module) && !Directory.Exists(module)) + { + logger.LogWarning($"{module} not found"); + continue; + } + var moduleFolderPath = module; + if (File.Exists(module)) + { + var extension = Path.GetExtension(module); + if (extension == ".psd1") + { + // ImportPSModulesFromPath doesn't like the direct path to the .psd1 file + // It only works when given a path to a folder that contains a .psd1 file with the same name as the enclosing folder + // To fix this, we make a copy of the psd1 file and give it the name of the parent folder + var parentFolder = Directory.GetParent(module); + var destinationPath = Path.Combine(parentFolder.FullName, parentFolder.Name + ".psd1"); + if (!File.Exists(destinationPath)) + { + File.Move(module, destinationPath); + logger.LogDebug($"Corrected path to {module}"); + } + moduleFolderPath = parentFolder.FullName; + } + } + + hostStartupInfo.InitialSessionState.ImportPSModulesFromPath(moduleFolderPath); + var loadedModule = hostStartupInfo.InitialSessionState.Modules.FirstOrDefault(a => a.Name.StartsWith(moduleFolderPath)); + if (loadedModule is null) + { + logger.LogWarning($"Error loading {module} from {moduleFolderPath}"); + } + } + // Autocomplete will fail if there isn't an implementation of TabExpansion2 + // The default TabExpansion2 implementation may not be available in a Constrained Runspace, therefore we check and add it if not. + // Note: Attempting to set the visibility of these commands to Private will cause Autocomplete to fail + if (!hostStartupInfo.InitialSessionState.Commands.Any(a => a.Name.ToLower() == "tabexpansion2")) + { + var defaultSessionState = InitialSessionState.CreateDefault2(); + var defaultTabExpansionFunctionEntry = defaultSessionState.Commands.FirstOrDefault(a => a.Name.ToLower() == "tabexpansion2"); + hostStartupInfo.InitialSessionState.Commands.Add(defaultTabExpansionFunctionEntry); + } + if (!hostStartupInfo.InitialSessionState.Commands.Any(a => a.Name.ToLower() == "prompt")) + { + var defaultSessionState = InitialSessionState.CreateDefault2(); + var defaultTabExpansionFunctionEntry = defaultSessionState.Commands.FirstOrDefault(a => a.Name.ToLower() == "prompt"); + hostStartupInfo.InitialSessionState.Commands.Add(defaultTabExpansionFunctionEntry); + } + // Adding Get-Command to the Runspace in case the calling runspace didn't add it. + // PSES calls Get-Command by its Module Qualified Syntax "Microsoft.PowerShell.Core\Get-Command" + // This fails in a Constrained Runspace. + // To work around it without modifying PSES code, we add Microsoft.PowerShell.Core\Get-Command as an alias to Get-Command. + hostStartupInfo.InitialSessionState.AddCommandAndAliasToInitialSessionState(@"Microsoft.PowerShell.Core\Get-Command"); + hostStartupInfo.InitialSessionState.AddCommandAndAliasToInitialSessionState(@"Microsoft.PowerShell.Core\Get-Help"); + hostStartupInfo.InitialSessionState.AddCommandAndAliasToInitialSessionState(@"Microsoft.PowerShell.Core\Get-Module"); + hostStartupInfo.InitialSessionState.AddCommandAndAliasToInitialSessionState(@"Microsoft.PowerShell.Core\Out-Default"); + + initialRunspace = CreateRunspace(hostStartupInfo.PSHost, hostStartupInfo.InitialSessionState); + } + else + { + logger.LogTrace("Creating initial PowerShell runspace"); + initialRunspace = CreateRunspace(psHost, hostStartupInfo.InitialSessionState); + } + logger.LogInformation("Opening Runspace"); + initialRunspace.Open(); Validate.IsNotNull("initialRunspace", initialRunspace); - this.logger.LogTrace($"Initializing PowerShell context with runspace {initialRunspace.Name}"); + this.logger.LogInformation($"Initializing PowerShell context with runspace {initialRunspace.Name}"); this.ownsInitialRunspace = ownsInitialRunspace; this.SessionState = PowerShellContextState.NotStarted; - this.ConsoleWriter = consoleHost; - this.ConsoleReader = consoleHost as IHostInput; + this.ConsoleWriter = hostUserInterface; + this.ConsoleReader = hostUserInterface as IHostInput; // Get the PowerShell runtime version this.LocalPowerShellVersion = PowerShellVersionDetails.GetVersionDetails( initialRunspace, this.logger); - this.powerShell = PowerShell.Create(); this.powerShell.Runspace = initialRunspace; @@ -342,7 +447,12 @@ public void Initialize( RunspaceContext.Original, connectionString: null); this.CurrentRunspace = this.initialRunspace; - + // Respect a user provided bundled module path. + if (Directory.Exists(hostStartupInfo.BundledModulePath)) + { + logger.LogTrace($"Using new bundled module path: {hostStartupInfo.BundledModulePath}"); + s_bundledModulePath = hostStartupInfo.BundledModulePath; + } // Write out the PowerShell version for tracking purposes this.logger.LogInformation($"PowerShell Version: {this.LocalPowerShellVersion.Version}, Edition: {this.LocalPowerShellVersion.Edition}"); @@ -365,7 +475,7 @@ public void Initialize( this.ConfigureRunspaceCapabilities(this.CurrentRunspace); // Set the $profile variable in the runspace - this.profilePaths = profilePaths; + this.profilePaths = hostStartupInfo.ProfilePaths; if (profilePaths != null) { this.SetProfileVariableInCurrentRunspace(profilePaths); @@ -391,7 +501,6 @@ public void Initialize( .PSVariable .GetValue("Host") as PSHost; - // Now that the runspace is ready, enqueue it for first use this.PromptNest = new PromptNest( this, @@ -400,9 +509,10 @@ public void Initialize( this.versionSpecificOperations); this.InvocationEventQueue = InvocationEventQueue.Create(this, this.PromptNest); + if (powerShellVersion.Major >= 5 && this.isPSReadLineEnabled && - PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, s_bundledModulePath, out PSReadLineProxy proxy)) + PSReadLinePromptContext.TryGetPSReadLineProxy(logger, out PSReadLineProxy proxy)) { this.PromptContext = new PSReadLinePromptContext( this, @@ -415,25 +525,36 @@ public void Initialize( this.PromptContext = new LegacyReadLineContext(this); } - // Finally, restore the runspace's execution policy to the user's policy instead of - // Bypass. - this.RestoreExecutionPolicy(); - } + if (!preloadModules) + { + // 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 modulesToImport) + { + var command = + new PSCommand() + .AddCommand("Microsoft.PowerShell.Core\\Import-Module") + .AddParameter("Name", module); - /// - /// Imports the PowerShellEditorServices.Commands module into - /// the runspace. This method will be moved somewhere else soon. - /// - /// - public Task ImportCommandsModuleAsync() +#pragma warning disable CS4014 + // This call queues the loading on the pipeline thread, so no need to await + this.ExecuteCommandAsync( + command, + sendOutputToHost: false, + sendErrorToHost: true).GetAwaiter().GetResult(); +#pragma warning restore CS4014 + } + } + } + public void ImportModule(string path) { - this.logger.LogTrace($"Importing PowershellEditorServices commands from {s_commandsModulePath}"); + this.logger.LogTrace($"Importing module {path}"); PSCommand importCommand = new PSCommand() .AddCommand("Import-Module") - .AddArgument(s_commandsModulePath); + .AddArgument(path); - return this.ExecuteCommandAsync(importCommand, sendOutputToHost: false, sendErrorToHost: false); + this.ExecuteCommandAsync(importCommand, sendOutputToHost: false, sendErrorToHost: false).ConfigureAwait(false).GetAwaiter().GetResult(); } private static bool CheckIfRunspaceNeedsEventHandlers(RunspaceDetails runspaceDetails) @@ -592,7 +713,7 @@ public async Task> ExecuteCommandAsync( // cancelled prompt when it's called again. if (executionOptions.AddToHistory) { - this.PromptContext.AddToHistory(executionOptions.InputString ?? psCommand.Commands[0].CommandText); + this.PromptContext.AddToHistory(executionOptions.InputString ?? psCommand.Commands [0].CommandText); } bool hadErrors = false; @@ -646,7 +767,7 @@ public async Task> ExecuteCommandAsync( // Instruct PowerShell to send output and errors to the host if (executionOptions.WriteOutputToHost) { - psCommand.Commands[0].MergeMyResults( + psCommand.Commands [0].MergeMyResults( PipelineResultTypes.Error, PipelineResultTypes.Output); @@ -681,7 +802,7 @@ public async Task> ExecuteCommandAsync( if (executionOptions.WriteInputToHost) { this.WriteOutput( - executionOptions.InputString ?? psCommand.Commands[0].CommandText, + executionOptions.InputString ?? psCommand.Commands [0].CommandText, includeNewLine: true); } @@ -799,7 +920,10 @@ public async Task> ExecuteCommandAsync( var errorMessage = strBld.ToString(); errorMessages?.Append(errorMessage); - this.logger.LogError(errorMessage); + if(executionOptions.WriteErrorsToHost) + { + this.logger.LogError(errorMessage); + } hadErrors = true; } @@ -994,7 +1118,7 @@ public Task> ExecuteScriptStringAsync( Validate.IsNotNull(nameof(scriptString), scriptString); PSCommand command = null; - if(CurrentRunspace.Runspace.SessionStateProxy.LanguageMode != PSLanguageMode.FullLanguage) + if (CurrentRunspace.Runspace.SessionStateProxy.LanguageMode != PSLanguageMode.FullLanguage) { try { @@ -1009,7 +1133,7 @@ public Task> ExecuteScriptStringAsync( } // fall back to old behavior - if(command == null) + if (command == null) { command = new PSCommand().AddScript(scriptString.Trim()); } @@ -1096,10 +1220,10 @@ public async Task ExecuteScriptWithArgsAsync(string script, string arguments = n command.AddCommand(script, false); } - + StringBuilder sb = new StringBuilder(); await this.ExecuteCommandAsync( command, - errorMessages: null, + errorMessages: sb, new ExecutionOptions { WriteInputToHost = true, @@ -1107,6 +1231,8 @@ await this.ExecuteCommandAsync( WriteErrorsToHost = true, AddToHistory = true, }).ConfigureAwait(false); + if (!string.IsNullOrEmpty(sb.ToString())) + logger.LogError(sb.ToString()); } /// @@ -1445,7 +1571,7 @@ private ExecutionTarget GetExecutionTarget(ExecutionOptions options = null) // We can't take the pipeline from PSReadLine if it's in a remote session, so we need to // invoke locally in that case. - if (IsDebuggerStopped && PromptNest.IsInDebugger && !(options.IsReadLine && PromptNest.IsRemote)) + if (PromptNest.IsInDebugger && IsDebuggerStopped && !(options.IsReadLine && PromptNest.IsRemote)) { return ExecutionTarget.Debugger; } @@ -1707,9 +1833,9 @@ internal static string QuoteEscapeString(string escapedPath) internal static string WildcardEscapePath(string path, bool escapeSpaces = false) { var sb = new StringBuilder(); - for (int i = 0; i < path.Length; i++) + for (int i = 0;i < path.Length;i++) { - char curr = path[i]; + char curr = path [i]; switch (curr) { // Escape '[', ']', '?' and '*' with '`' @@ -1760,14 +1886,14 @@ internal static string UnescapeWildcardEscapedPath(string wildcardEscapedPath) } var sb = new StringBuilder(wildcardEscapedPath.Length); - for (int i = 0; i < wildcardEscapedPath.Length; i++) + for (int i = 0;i < wildcardEscapedPath.Length;i++) { // If we see a backtick perform a lookahead - char curr = wildcardEscapedPath[i]; + char curr = wildcardEscapedPath [i]; if (curr == '`' && i + 1 < wildcardEscapedPath.Length) { // If the next char is an escapable one, don't add this backtick to the new string - char next = wildcardEscapedPath[i + 1]; + char next = wildcardEscapedPath [i + 1]; switch (next) { case '[': @@ -2169,14 +2295,14 @@ internal void RestoreExecutionPolicy() // set to expected values, so we must sift through those. ExecutionPolicy policyToSet = ExecutionPolicy.Bypass; - var currentUserPolicy = (ExecutionPolicy)policies[policies.Count - 2].Members["ExecutionPolicy"].Value; + var currentUserPolicy = (ExecutionPolicy)policies [policies.Count - 2].Members ["ExecutionPolicy"].Value; if (currentUserPolicy != ExecutionPolicy.Undefined) { policyToSet = currentUserPolicy; } else { - var localMachinePolicy = (ExecutionPolicy)policies[policies.Count - 1].Members["ExecutionPolicy"].Value; + var localMachinePolicy = (ExecutionPolicy)policies [policies.Count - 1].Members ["ExecutionPolicy"].Value; if (localMachinePolicy != ExecutionPolicy.Undefined) { policyToSet = localMachinePolicy; @@ -2348,7 +2474,7 @@ private void HandleRunspaceStateChanged(object sender, RunspaceStateEventArgs ar case RunspaceState.Closed: case RunspaceState.Broken: // If the runspace closes or fails, pop the runspace - ((IHostSupportsInteractiveSession)this).PopRunspace(); + ((IHostSupportsInteractiveSession) this).PopRunspace(); break; } } @@ -2360,7 +2486,7 @@ private static IEnumerable GetLoadableProfilePaths(ProfilePathInfo profi yield break; } - foreach (string path in new [] { profilePaths.AllUsersAllHosts, profilePaths.AllUsersCurrentHost, profilePaths.CurrentUserAllHosts, profilePaths.CurrentUserCurrentHost }) + foreach (string path in new[] { profilePaths.AllUsersAllHosts, profilePaths.AllUsersCurrentHost, profilePaths.CurrentUserAllHosts, profilePaths.CurrentUserCurrentHost }) { if (path != null && File.Exists(path)) { @@ -2392,12 +2518,12 @@ private void StartCommandLoopOnRunspaceAvailable() void availabilityChangedHandler(object runspace, RunspaceAvailabilityEventArgs eventArgs) { if (eventArgs.RunspaceAvailability != RunspaceAvailability.Available || - this.versionSpecificOperations.IsDebuggerStopped(this.PromptNest, (Runspace)runspace)) + this.versionSpecificOperations.IsDebuggerStopped(this.PromptNest, (Runspace) runspace)) { return; } - ((Runspace)runspace).AvailabilityChanged -= availabilityChangedHandler; + ((Runspace) runspace).AvailabilityChanged -= availabilityChangedHandler; Interlocked.Exchange(ref this.isCommandLoopRestarterSet, 0); this.ConsoleReader?.StartCommandLoop(); } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs index 89203e5c1..fc029ac42 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/Host/ProtocolPSHostUserInterface.cs @@ -14,6 +14,7 @@ internal class ProtocolPSHostUserInterface : EditorServicesPSHostUserInterface #region Private Fields private readonly ILanguageServerFacade _languageServer; + private readonly ILogger _logger; #endregion @@ -34,6 +35,7 @@ public ProtocolPSHostUserInterface( logger) { _languageServer = languageServer; + _logger = logger; } #endregion @@ -65,7 +67,28 @@ public override void WriteOutput( ConsoleColor foregroundColor, ConsoleColor backgroundColor) { - // TODO: Invoke the "output" notification! + if(includeNewLine) + { + outputString = "\r\n" + outputString; + } + switch(outputType) + { + case OutputType.Normal: + _logger.LogInformation(outputString); + break; + case OutputType.Error: + _logger.LogError(outputString); + break; + case OutputType.Warning: + _logger.LogWarning(outputString); + break; + case OutputType.Verbose: + _logger.LogTrace(outputString); + break; + case OutputType.Debug: + _logger.LogDebug(outputString); + break; + } } /// diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs index d58afc7f1..dfc920ec1 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -15,12 +15,14 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext using System.IO; using System.Management.Automation; - internal class PSReadLinePromptContext : IPromptContext + internal class PSReadLinePromptContext: IPromptContext { private static readonly Lazy s_lazyInvokeReadLineForEditorServicesCmdletInfo = new Lazy(() => { - var type = Type.GetType("Microsoft.PowerShell.EditorServices.Commands.InvokeReadLineForEditorServicesCommand, Microsoft.PowerShell.EditorServices.Hosting"); - return new CmdletInfo("__Invoke-ReadLineForEditorServices", type); + var allAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + var assemblies = allAssemblies.FirstOrDefault(a => a.FullName.Contains("Microsoft.PowerShell.EditorServices")); + var type = assemblies?.ExportedTypes?.FirstOrDefault(a => a.Name == "InvokeReadLineForEditorServicesCommand"); + return new CmdletInfo("__Invoke-ReadLineForEditorServices", type ?? typeof(PSCmdlet)); }); private static ExecutionOptions s_psrlExecutionOptions = new ExecutionOptions @@ -64,59 +66,46 @@ internal PSReadLinePromptContext( internal static bool TryGetPSReadLineProxy( ILogger logger, - Runspace runspace, - string bundledModulePath, out PSReadLineProxy readLineProxy) { readLineProxy = null; logger.LogTrace("Attempting to load PSReadLine"); - using (var pwsh = PowerShell.Create()) - { - pwsh.Runspace = runspace; - pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", Path.Combine(bundledModulePath, "PSReadLine")) - .Invoke(); - - if (pwsh.HadErrors) - { - logger.LogWarning("PSConsoleReadline type not found: {Reason}", pwsh.Streams.Error[0].ToString()); - return false; - } - - var psReadLineType = Type.GetType("Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2"); + var psReadLineType = Type.GetType("Microsoft.PowerShell.PSConsoleReadLine, Microsoft.PowerShell.PSReadLine2"); + if (psReadLineType == null) + { + // NOTE: For some reason `Type.GetType(...)` can fail to find the type, + // and in that case, this search through the `AppDomain` for some reason will succeed. + // It's slower, but only happens when needed. + logger.LogTrace("PSConsoleReadline type not found using Type.GetType(), searching all loaded assemblies..."); + psReadLineType = AppDomain.CurrentDomain + .GetAssemblies() + .FirstOrDefault(asm => asm.GetName().Name.Equals("Microsoft.PowerShell.PSReadLine2")) + ?.ExportedTypes + ?.FirstOrDefault(type => type.FullName.Equals("Microsoft.PowerShell.PSConsoleReadLine")); if (psReadLineType == null) { - // NOTE: For some reason `Type.GetType(...)` can fail to find the type, - // and in that case, this search through the `AppDomain` for some reason will succeed. - // It's slower, but only happens when needed. - logger.LogTrace("PSConsoleReadline type not found using Type.GetType(), searching all loaded assemblies..."); - psReadLineType = AppDomain.CurrentDomain - .GetAssemblies() - .FirstOrDefault(asm => asm.GetName().Name.Equals("Microsoft.PowerShell.PSReadLine2")) - ?.ExportedTypes - ?.FirstOrDefault(type => type.FullName.Equals("Microsoft.PowerShell.PSConsoleReadLine")); - if (psReadLineType == null) - { - logger.LogWarning("PSConsoleReadLine type not found anywhere!"); - return false; - } - } - - try - { - readLineProxy = new PSReadLineProxy(psReadLineType, logger); - } - catch (InvalidOperationException e) - { - // The Type we got back from PowerShell doesn't have the members we expected. - // Could be an older version, a custom build, or something a newer version with - // breaking changes. - logger.LogWarning("PSReadLineProxy unable to be initialized: {Reason}", e); + logger.LogWarning("PSConsoleReadLine type not found anywhere!"); return false; } } - + try + { + readLineProxy = new PSReadLineProxy(psReadLineType, logger); + } + catch (InvalidOperationException e) + { + // The Type we got back from PowerShell doesn't have the members we expected. + // Could be an older version, a custom build, or something a newer version with + // breaking changes. + logger.LogWarning("PSReadLineProxy unable to be initialized: {Reason}", e); + return false; + } + catch (CommandNotFoundException e) + { + logger.LogWarning("PSReadLineProxy unable to be initialized: {Reason}", e); + return false; + } return true; } @@ -164,7 +153,8 @@ public void AbortReadLine() WaitForReadLineExit(); } - public async Task AbortReadLineAsync() { + public async Task AbortReadLineAsync() + { if (_readLineCancellationSource == null) { return; @@ -181,7 +171,8 @@ public void WaitForReadLineExit() { } } - public async Task WaitForReadLineExitAsync() { + public async Task WaitForReadLineExitAsync() + { using (await _promptNest.GetRunspaceHandleAsync(isReadLine: true, CancellationToken.None).ConfigureAwait(false)) { } } diff --git a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs index 422cc58a0..9751769eb 100644 --- a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs +++ b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs @@ -38,9 +38,8 @@ internal static class PowerShellContextFactory public static System.Management.Automation.Runspaces.Runspace InitialRunspace; - public static PowerShellContextService Create(ILogger logger) + public static PowerShellContextService Create(ILogger logger, bool isPSReadLineEnabled = false) { - PowerShellContextService powerShellContext = new PowerShellContextService(logger, null, isPSReadLineEnabled: false); var initialSessionState = InitialSessionState.CreateDefault(); // We set the process scope's execution policy (which is really the runspace's scope) to // `Bypass` so we can import our bundled modules. This is equivalent in scope to the CLI @@ -67,6 +66,8 @@ public static PowerShellContextService Create(ILogger logger) usesLegacyReadLine: false, bundledModulePath: BundledModulePath); + PowerShellContextService powerShellContext = new PowerShellContextService(logger, null, isPSReadLineEnabled); + InitialRunspace = PowerShellContextService.CreateTestRunspace( testHostDetails, powerShellContext, @@ -74,10 +75,9 @@ public static PowerShellContextService Create(ILogger logger) logger); powerShellContext.Initialize( - TestProfilePaths, - InitialRunspace, - ownsInitialRunspace: true, - consoleHost: null); + testHostDetails, + null, + ownsInitialRunspace: true); return powerShellContext; } diff --git a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs index fc15c7854..247c4cf45 100644 --- a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs @@ -16,7 +16,7 @@ namespace Microsoft.PowerShell.EditorServices.Test.Console { - public class PowerShellContextTests : IDisposable + public class PowerShellContextTests: IDisposable { // Borrowed from `VersionUtils` which can't be used here due to an initialization problem. private static bool IsWindows { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); @@ -26,7 +26,7 @@ public class PowerShellContextTests : IDisposable private static readonly string s_debugTestFilePath = TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Debugging/DebugTest.ps1"); - + public PowerShellContextTests() { this.powerShellContext = PowerShellContextFactory.Create(NullLogger.Instance); @@ -110,8 +110,8 @@ public async Task CanAbortExecution() [Fact] public async Task CanResolveAndLoadProfilesForHostId() { - string[] expectedProfilePaths = - new string[] + string [] expectedProfilePaths = + new string [] { PowerShellContextFactory.TestProfilePaths.AllUsersAllHosts, PowerShellContextFactory.TestProfilePaths.AllUsersCurrentHost, @@ -131,7 +131,7 @@ public async Task CanResolveAndLoadProfilesForHostId() "$($profile.CurrentUserAllHosts) " + "$($profile.CurrentUserCurrentHost) " + "$(Assert-ProfileLoaded)\""); - + var result = await this.powerShellContext.ExecuteCommandAsync( psCommand).ConfigureAwait(false); @@ -150,10 +150,10 @@ await this.powerShellContext.ExecuteCommandAsync( [Fact] public void CanGetPSReadLineProxy() { + // This will force the loading of the PSReadLine assembly + var psContext = PowerShellContextFactory.Create(NullLogger.Instance, isPSReadLineEnabled: true); Assert.True(PSReadLinePromptContext.TryGetPSReadLineProxy( NullLogger.Instance, - PowerShellContextFactory.InitialRunspace, - PowerShellContextFactory.BundledModulePath, out PSReadLineProxy proxy)); }