diff --git a/PowerShellEditorServices.Common.props b/PowerShellEditorServices.Common.props index 2ab30ebc7..cdb72f19d 100644 --- a/PowerShellEditorServices.Common.props +++ b/PowerShellEditorServices.Common.props @@ -11,6 +11,5 @@ git https://github.com/PowerShell/PowerShellEditorServices portable - $(DefineConstants);$(ExtraDefineConstants) diff --git a/PowerShellEditorServices.build.ps1 b/PowerShellEditorServices.build.ps1 index 8df6c5fc1..fbd126708 100644 --- a/PowerShellEditorServices.build.ps1 +++ b/PowerShellEditorServices.build.ps1 @@ -259,20 +259,20 @@ task TestServer TestServerWinPS,TestServerPS7,TestServerPS72 task TestServerWinPS -If (-not $script:IsNix) { Set-Location .\test\PowerShellEditorServices.Test\ - exec { & $script:dotnetExe test -p:ExtraDefineConstants=TEST --logger trx -f $script:NetRuntime.Desktop (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.Desktop (DotNetTestFilter) } } task TestServerPS7 -If (-not $script:IsRosetta) { Set-Location .\test\PowerShellEditorServices.Test\ Invoke-WithCreateDefaultHook -NewModulePath $script:PSCoreModulePath { - exec { & $script:dotnetExe test -p:ExtraDefineConstants=TEST --logger trx -f $script:NetRuntime.PS7 (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.PS7 (DotNetTestFilter) } } } task TestServerPS72 { Set-Location .\test\PowerShellEditorServices.Test\ Invoke-WithCreateDefaultHook -NewModulePath $script:PSCoreModulePath { - exec { & $script:dotnetExe test -p:ExtraDefineConstants=TEST --logger trx -f $script:NetRuntime.PS72 (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.PS72 (DotNetTestFilter) } } } @@ -281,13 +281,13 @@ task TestE2E { $env:PWSH_EXE_NAME = if ($IsCoreCLR) { "pwsh" } else { "powershell" } $NetRuntime = if ($IsRosetta) { $script:NetRuntime.PS72 } else { $script:NetRuntime.PS7 } - exec { & $script:dotnetExe test -p:ExtraDefineConstants=TEST --logger trx -f $NetRuntime (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $NetRuntime (DotNetTestFilter) } # Run E2E tests in ConstrainedLanguage mode. if (!$script:IsNix) { try { [System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", "0x80000007", [System.EnvironmentVariableTarget]::Machine); - exec { & $script:dotnetExe test -p:ExtraDefineConstants=TEST --logger trx -f $script:NetRuntime.PS7 (DotNetTestFilter) } + exec { & $script:dotnetExe test --logger trx -f $script:NetRuntime.PS7 (DotNetTestFilter) } } finally { [System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", $null, [System.EnvironmentVariableTarget]::Machine); } diff --git a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs index b4dbf7c63..e49da8527 100644 --- a/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs +++ b/src/PowerShellEditorServices.Hosting/Internal/EditorServicesRunner.cs @@ -292,7 +292,8 @@ private HostStartupInfo CreateHostStartupInfo() _config.LogPath, (int)_config.LogLevel, consoleReplEnabled: _config.ConsoleRepl != ConsoleReplKind.None, - usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine); + usesLegacyReadLine: _config.ConsoleRepl == ConsoleReplKind.LegacyReadLine, + bundledModulePath: _config.BundledModulePath); } private void WriteStartupBanner() diff --git a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs index 5f3ae7647..9fc788e0d 100644 --- a/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs +++ b/src/PowerShellEditorServices/Hosting/HostStartupInfo.cs @@ -107,6 +107,11 @@ public sealed class HostStartupInfo /// public int LogLevel { get; } + /// + /// The path to find the bundled modules. User configurable for advanced usage. + /// + public string BundledModulePath { get; } + #endregion #region Constructors @@ -135,6 +140,7 @@ public sealed class HostStartupInfo /// The minimum log event level. /// Enable console if true. /// Use PSReadLine if false, otherwise use the legacy readline implementation. + /// A custom path to the expected bundled modules. public HostStartupInfo( string name, string profileId, @@ -147,7 +153,8 @@ public HostStartupInfo( string logPath, int logLevel, bool consoleReplEnabled, - bool usesLegacyReadLine) + bool usesLegacyReadLine, + string bundledModulePath) { Name = name ?? DefaultHostName; ProfileId = profileId ?? DefaultHostProfileId; @@ -161,6 +168,7 @@ public HostStartupInfo( LogLevel = logLevel; ConsoleReplEnabled = consoleReplEnabled; UsesLegacyReadLine = usesLegacyReadLine; + BundledModulePath = bundledModulePath; } #endregion diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs index f226dd8d3..198cd26d9 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/PowerShellContextService.cs @@ -11,6 +11,7 @@ using System.Management.Automation.Remoting; using System.Management.Automation.Runspaces; using System.Reflection; +using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -32,10 +33,18 @@ namespace Microsoft.PowerShell.EditorServices.Services /// internal class PowerShellContextService : IHostSupportsInteractiveSession { - private static readonly string s_commandsModulePath = Path.GetFullPath( - Path.Combine( - Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), - "../../Commands/PowerShellEditorServices.Commands.psd1")); + // This is a default that can be overriden at runtime by the user or tests. + private static string s_bundledModulePath = Path.GetFullPath(Path.Combine( + Path.GetDirectoryName(typeof(PowerShellContextService).Assembly.Location), + "..", + "..", + "..")); + + private static string s_commandsModulePath => Path.GetFullPath(Path.Combine( + s_bundledModulePath, + "PowerShellEditorServices", + "Commands", + "PowerShellEditorServices.Commands.psd1")); private static readonly Action s_runspaceApartmentStateSetter; private static readonly PropertyInfo s_writeStreamProperty; @@ -189,9 +198,16 @@ public static PowerShellContextService Create( OmniSharp.Extensions.LanguageServer.Protocol.Server.ILanguageServerFacade languageServer, HostStartupInfo hostStartupInfo) { + var logger = factory.CreateLogger(); + Validate.IsNotNull(nameof(hostStartupInfo), hostStartupInfo); - var logger = factory.CreateLogger(); + // 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; @@ -281,6 +297,15 @@ public static Runspace CreateRunspace(PSHost psHost, PSLanguageMode languageMode // should have the same LanguageMode of whatever is set by the system. initialSessionState.LanguageMode = languageMode; + // 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 + // argument `-Bypass`, which (for instance) the extension passes. Thus we emulate this + // behavior for consistency such that unit tests can pass in a similar environment. + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + initialSessionState.ExecutionPolicy = ExecutionPolicy.Bypass; + } + Runspace runspace = RunspaceFactory.CreateRunspace(psHost, initialSessionState); // Windows PowerShell must be hosted in STA mode @@ -396,7 +421,7 @@ public void Initialize( if (powerShellVersion.Major >= 5 && this.isPSReadLineEnabled && - PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, out PSReadLineProxy proxy)) + PSReadLinePromptContext.TryGetPSReadLineProxy(logger, initialRunspace, s_bundledModulePath, out PSReadLineProxy proxy)) { this.PromptContext = new PSReadLinePromptContext( this, @@ -420,15 +445,13 @@ public void Initialize( /// the runspace. This method will be moved somewhere else soon. /// /// - public Task ImportCommandsModuleAsync() => ImportCommandsModuleAsync(s_commandsModulePath); - - public Task ImportCommandsModuleAsync(string path) + public Task ImportCommandsModuleAsync() { - this.logger.LogTrace($"Importing PowershellEditorServices commands from {path}"); + this.logger.LogTrace($"Importing PowershellEditorServices commands from {s_commandsModulePath}"); PSCommand importCommand = new PSCommand() .AddCommand("Import-Module") - .AddArgument(path); + .AddArgument(s_commandsModulePath); return this.ExecuteCommandAsync(importCommand, sendOutputToHost: false, sendErrorToHost: false); } diff --git a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs index 5f4931854..ee4c50634 100644 --- a/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs +++ b/src/PowerShellEditorServices/Services/PowerShellContext/Session/PSReadLinePromptContext.cs @@ -17,20 +17,6 @@ namespace Microsoft.PowerShell.EditorServices.Services.PowerShellContext internal class PSReadLinePromptContext : IPromptContext { - private static readonly string _psReadLineModulePath = Path.Combine( - Path.GetDirectoryName(typeof(PSReadLinePromptContext).Assembly.Location), - "..", - "..", - "..", -#if TEST - // When using xUnit (dotnet test) the assemblies are deployed to the - // test project folder, invalidating our relative path assumption. - "..", - "..", - "module", -#endif - "PSReadLine"); - private static readonly Lazy s_lazyInvokeReadLineForEditorServicesCmdletInfo = new Lazy(() => { var type = Type.GetType("Microsoft.PowerShell.EditorServices.Commands.InvokeReadLineForEditorServicesCommand, Microsoft.PowerShell.EditorServices.Hosting"); @@ -79,6 +65,7 @@ internal PSReadLinePromptContext( internal static bool TryGetPSReadLineProxy( ILogger logger, Runspace runspace, + string bundledModulePath, out PSReadLineProxy readLineProxy) { readLineProxy = null; @@ -87,15 +74,33 @@ internal static bool TryGetPSReadLineProxy( { pwsh.Runspace = runspace; pwsh.AddCommand("Microsoft.PowerShell.Core\\Import-Module") - .AddParameter("Name", _psReadLineModulePath) + .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"); if (psReadLineType == null) { - logger.LogWarning("PSConsoleReadline type not found: {Reason}", pwsh.HadErrors ? pwsh.Streams.Error[0].ToString() : ""); - return false; + // 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 diff --git a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj index 2c443e008..57a0514b6 100644 --- a/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj +++ b/test/PowerShellEditorServices.Test.E2E/PowerShellEditorServices.Test.E2E.csproj @@ -22,8 +22,6 @@ - - PreserveNewest - + diff --git a/test/PowerShellEditorServices.Test.E2E/xunit.runner.json b/test/PowerShellEditorServices.Test.E2E/xunit.runner.json index 79d1ad980..3f3645a0a 100644 --- a/test/PowerShellEditorServices.Test.E2E/xunit.runner.json +++ b/test/PowerShellEditorServices.Test.E2E/xunit.runner.json @@ -1,4 +1,6 @@ { - "parallelizeTestCollections": false + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "appDomain": "denied", + "parallelizeTestCollections": false, + "methodDisplay": "method" } - diff --git a/test/PowerShellEditorServices.Test/App.config b/test/PowerShellEditorServices.Test/App.config deleted file mode 100644 index 9735dc735..000000000 --- a/test/PowerShellEditorServices.Test/App.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs b/test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs index 42fbaacb2..2111a5e88 100644 --- a/test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs +++ b/test/PowerShellEditorServices.Test/Language/SemanticTokenTest.cs @@ -17,7 +17,7 @@ namespace Microsoft.PowerShell.EditorServices.Test.Language public class SemanticTokenTest { [Fact] - public async Task TokenizesFunctionElements() + public void TokenizesFunctionElements() { string text = @" function Get-Sum { @@ -59,7 +59,7 @@ function Get-Sum { } [Fact] - public async Task TokenizesStringExpansion() + public void TokenizesStringExpansion() { string text = "Write-Host \"$(Test-Property Get-Whatever) $(Get-Whatever)\""; ScriptFile scriptFile = new ScriptFile( @@ -82,7 +82,7 @@ public async Task TokenizesStringExpansion() } [Fact] - public async Task RecognizesTokensWithAsterisk() + public void RecognizesTokensWithAsterisk() { string text = @" function Get-A*A { @@ -111,7 +111,7 @@ function Get-A*A { } [Fact] - public async Task RecognizesArrayPropertyInExpandableString() + public void RecognizesArrayPropertyInExpandableString() { string text = "\"$(@($Array).Count) OtherText\""; ScriptFile scriptFile = new ScriptFile( @@ -136,7 +136,7 @@ public async Task RecognizesArrayPropertyInExpandableString() } [Fact] - public async Task RecognizesCurlyQuotedString() + public void RecognizesCurlyQuotedString() { string text = "“^[-'a-z]*”"; ScriptFile scriptFile = new ScriptFile( @@ -150,7 +150,7 @@ public async Task RecognizesCurlyQuotedString() } [Fact] - public async Task RecognizeEnum() + public void RecognizeEnum() { string text = @" enum MyEnum{ diff --git a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs index 4eb93395a..41c3e4730 100644 --- a/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs +++ b/test/PowerShellEditorServices.Test/PowerShellContextFactory.cs @@ -32,7 +32,10 @@ internal static class PowerShellContextFactory Path.GetFullPath( TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/ProfileTest.ps1"))); - public static System.Management.Automation.Runspaces.Runspace initialRunspace; + public static readonly string BundledModulePath = Path.GetFullPath( + TestUtilities.NormalizePath("../../../../../module")); + + public static System.Management.Automation.Runspaces.Runspace InitialRunspace; public static PowerShellContextService Create(ILogger logger) { @@ -46,13 +49,16 @@ public static PowerShellContextService Create(ILogger logger) TestProfilePaths, new List(), new List(), + // TODO: We want to replace this property with an entire initial session state, + // which would then also control the process-scoped execution policy. PSLanguageMode.FullLanguage, null, 0, consoleReplEnabled: false, - usesLegacyReadLine: false); + usesLegacyReadLine: false, + bundledModulePath: BundledModulePath); - initialRunspace = PowerShellContextService.CreateRunspace( + InitialRunspace = PowerShellContextService.CreateRunspace( testHostDetails, powerShellContext, new TestPSHostUserInterface(powerShellContext, logger), @@ -60,7 +66,7 @@ public static PowerShellContextService Create(ILogger logger) powerShellContext.Initialize( TestProfilePaths, - initialRunspace, + InitialRunspace, ownsInitialRunspace: true, consoleHost: null); diff --git a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj index 70115e8f7..bbf7c3814 100644 --- a/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj +++ b/test/PowerShellEditorServices.Test/PowerShellEditorServices.Test.csproj @@ -1,27 +1,34 @@  + net6.0;netcoreapp3.1;net461 Microsoft.PowerShell.EditorServices.Test x64 + true true + + + + + @@ -30,14 +37,21 @@ + + PreserveNewest + + + + + $(DefineConstants);CoreCLR diff --git a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs index 4cf1fef8c..ba1e07611 100644 --- a/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs +++ b/test/PowerShellEditorServices.Test/Session/PowerShellContextTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Management.Automation; using System.Runtime.InteropServices; @@ -148,13 +147,13 @@ await this.powerShellContext.ExecuteCommandAsync( } [Trait("Category", "PSReadLine")] - [SkippableFact] - public async Task CanGetPSReadLineProxy() + [Fact] + public void CanGetPSReadLineProxy() { - Skip.If(IsWindows, "This test doesn't work on Windows for some reason."); Assert.True(PSReadLinePromptContext.TryGetPSReadLineProxy( NullLogger.Instance, - PowerShellContextFactory.initialRunspace, + PowerShellContextFactory.InitialRunspace, + PowerShellContextFactory.BundledModulePath, out PSReadLineProxy proxy)); } diff --git a/test/PowerShellEditorServices.Test/xunit.runner.json b/test/PowerShellEditorServices.Test/xunit.runner.json index 0b4dfc597..f8b76c8fc 100644 --- a/test/PowerShellEditorServices.Test/xunit.runner.json +++ b/test/PowerShellEditorServices.Test/xunit.runner.json @@ -1,4 +1,6 @@ { + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "appDomain": "denied", "parallelizeTestCollections": false, "methodDisplay": "method" }