diff --git a/module/PowerShellEditorServices/Start-EditorServices.ps1 b/module/PowerShellEditorServices/Start-EditorServices.ps1 index a3a448593..acb221f94 100644 --- a/module/PowerShellEditorServices/Start-EditorServices.ps1 +++ b/module/PowerShellEditorServices/Start-EditorServices.ps1 @@ -142,7 +142,8 @@ function WriteSessionFile($sessionInfo) { } # Are we running in PowerShell 2 or earlier? -if ($PSVersionTable.PSVersion.Major -le 2) { +$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 @@ -257,32 +258,6 @@ function Test-NamedPipeName { return !(Test-Path $path) } -function Set-NamedPipeMode { - param( - [Parameter(Mandatory=$true)] - [ValidateNotNullOrEmpty()] - [string] - $PipeFile - ) - - if (($PSVersionTable.PSVersion.Major -le 5) -or $IsWindows) { - return - } - - chmod $DEFAULT_USER_MODE $PipeFile - - if ($IsLinux) { - $mode = /usr/bin/stat -c "%a" $PipeFile - } - elseif ($IsMacOS) { - $mode = /usr/bin/stat -f "%A" $PipeFile - } - - if ($mode -ne $DEFAULT_USER_MODE) { - ExitWithError "Permissions to the pipe file were not set properly. Expected: $DEFAULT_USER_MODE Actual: $mode for file: $PipeFile" - } -} - LogSection "Console Encoding" Log $OutputEncoding @@ -316,9 +291,6 @@ function Set-PipeFileResult { ) $ResultTable[$PipeNameKey] = Get-NamedPipePath -PipeName $PipeNameValue - if (($PSVersionTable.PSVersion.Major -ge 6) -and ($IsLinux -or $IsMacOS)) { - Set-NamedPipeMode -PipeFile $ResultTable[$PipeNameKey] - } } # Add BundledModulesPath to $env:PSModulePath diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs index 07f2cfe39..1d3bd8a7d 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeClientChannel.cs @@ -16,6 +16,9 @@ public class NamedPipeClientChannel : ChannelBase private ILogger logger; private NamedPipeClientStream pipeClient; + // 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 = 536870912; + private const string NAMED_PIPE_UNIX_PREFIX = "CoreFxPipe_"; public NamedPipeClientChannel( @@ -56,10 +59,12 @@ public static async Task ConnectAsync( { string pipeName = System.IO.Path.GetFileName(pipeFile); + var options = PipeOptions.Asynchronous; // on macOS and Linux, the named pipe name is prefixed by .NET Core if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { pipeName = pipeFile.Split(new [] {NAMED_PIPE_UNIX_PREFIX}, StringSplitOptions.None)[1]; + options |= (PipeOptions)CurrentUserOnly; } var pipeClient = @@ -67,7 +72,7 @@ public static async Task ConnectAsync( ".", pipeName, PipeDirection.InOut, - PipeOptions.Asynchronous); + options); await pipeClient.ConnectAsync(); var clientChannel = new NamedPipeClientChannel(pipeClient, logger); @@ -77,4 +82,3 @@ public static async Task ConnectAsync( } } } - diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs index 1a49fcb61..4765b7f90 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.IO; using System.IO.Pipes; +using System.Reflection; using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; @@ -18,6 +19,15 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel { public class NamedPipeServerListener : ServerListenerBase { + // 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 = 536870912; + + // 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 ConstructorInfo _netFrameworkPipeServerConstructor = + typeof(NamedPipeServerStream).GetConstructor(new [] { typeof(string), typeof(PipeDirection), typeof(int), typeof(PipeTransmissionMode), typeof(PipeOptions), typeof(int), typeof(int), typeof(PipeSecurity) }); + private ILogger logger; private string inOutPipeName; private readonly string outPipeName; @@ -50,7 +60,8 @@ public override void Start() { try { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + // If we're running in Windows PowerShell, we use the constructor via Reflection + if (RuntimeInformation.FrameworkDescription.StartsWith(".NET Framework")) { PipeSecurity pipeSecurity = new PipeSecurity(); @@ -72,27 +83,41 @@ public override void Start() PipeAccessRights.ReadWrite, AccessControlType.Allow)); } - // Unfortunately, .NET Core does not support passing in a PipeSecurity object into the constructor for - // NamedPipeServerStream so we are creating native Named Pipes and securing them using native APIs. The - // issue on .NET Core regarding Named Pipe security is here: https://github.com/dotnet/corefx/issues/30170 - // 99% of this code was borrowed from PowerShell here: - // https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs#L124-L256 - this.inOutPipeServer = NamedPipeNative.CreateNamedPipe(inOutPipeName, pipeSecurity); + _netFrameworkPipeServerConstructor.Invoke(new object[] + { + inOutPipeName, + PipeDirection.InOut, + 1, // maxNumberOfServerInstances + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 1024, // inBufferSize + 1024, // outBufferSize + pipeSecurity + }); + if (this.outPipeName != null) { - this.outPipeServer = NamedPipeNative.CreateNamedPipe(outPipeName, pipeSecurity); + _netFrameworkPipeServerConstructor.Invoke(new object[] + { + outPipeName, + PipeDirection.InOut, + 1, // maxNumberOfServerInstances + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous, + 1024, // inBufferSize + 1024, // outBufferSize + pipeSecurity + }); } } else { - // This handles the Unix case since PipeSecurity is not supported on Unix. - // Instead, we use chmod in Start-EditorServices.ps1 this.inOutPipeServer = new NamedPipeServerStream( pipeName: inOutPipeName, direction: PipeDirection.InOut, maxNumberOfServerInstances: 1, transmissionMode: PipeTransmissionMode.Byte, - options: PipeOptions.Asynchronous); + options: PipeOptions.Asynchronous | (PipeOptions)CurrentUserOnly); if (this.outPipeName != null) { this.outPipeServer = new NamedPipeServerStream( @@ -100,7 +125,7 @@ public override void Start() direction: PipeDirection.Out, maxNumberOfServerInstances: 1, transmissionMode: PipeTransmissionMode.Byte, - options: PipeOptions.None); + options: (PipeOptions)CurrentUserOnly); } } ListenForConnection(); @@ -171,143 +196,4 @@ private static async Task WaitForConnectionAsync(NamedPipeServerStream pipeServe await pipeServerStream.FlushAsync(); } } - - /// - /// Native API for Named Pipes - /// This code was borrowed from PowerShell here: - /// https://github.com/PowerShell/PowerShell/blob/master/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs#L124-L256 - /// - internal static class NamedPipeNative - { - #region Pipe constants - - // Pipe open mode - internal const uint PIPE_ACCESS_DUPLEX = 0x00000003; - - // Pipe modes - internal const uint PIPE_TYPE_BYTE = 0x00000000; - internal const uint FILE_FLAG_OVERLAPPED = 0x40000000; - internal const uint FILE_FLAG_FIRST_PIPE_INSTANCE = 0x00080000; - internal const uint PIPE_READMODE_BYTE = 0x00000000; - - #endregion - - #region Data structures - - [StructLayout(LayoutKind.Sequential)] - internal class SECURITY_ATTRIBUTES - { - /// - /// The size, in bytes, of this structure. Set this value to the size of the SECURITY_ATTRIBUTES structure. - /// - public int NLength; - - /// - /// A pointer to a security descriptor for the object that controls the sharing of it. - /// - public IntPtr LPSecurityDescriptor = IntPtr.Zero; - - /// - /// A Boolean value that specifies whether the returned handle is inherited when a new process is created. - /// - public bool InheritHandle; - - /// - /// Initializes a new instance of the SECURITY_ATTRIBUTES class - /// - public SECURITY_ATTRIBUTES() - { - this.NLength = 12; - } - } - - #endregion - - #region Pipe methods - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - internal static extern SafePipeHandle CreateNamedPipe( - string lpName, - uint dwOpenMode, - uint dwPipeMode, - uint nMaxInstances, - uint nOutBufferSize, - uint nInBufferSize, - uint nDefaultTimeOut, - SECURITY_ATTRIBUTES securityAttributes); - - internal static SECURITY_ATTRIBUTES GetSecurityAttributes(GCHandle securityDescriptorPinnedHandle, bool inheritHandle = false) - { - SECURITY_ATTRIBUTES securityAttributes = new NamedPipeNative.SECURITY_ATTRIBUTES(); - securityAttributes.InheritHandle = inheritHandle; - securityAttributes.NLength = (int)Marshal.SizeOf(securityAttributes); - securityAttributes.LPSecurityDescriptor = securityDescriptorPinnedHandle.AddrOfPinnedObject(); - return securityAttributes; - } - - /// - /// Helper method to create a PowerShell transport named pipe via native API, along - /// with a returned .Net NamedPipeServerStream object wrapping the named pipe. - /// - /// Named pipe core name. - /// - /// NamedPipeServerStream - internal static NamedPipeServerStream CreateNamedPipe( - string pipeName, - PipeSecurity pipeSecurity) - - { - string fullPipeName = @"\\.\pipe\" + pipeName; - CommonSecurityDescriptor securityDesc = new CommonSecurityDescriptor(false, false, pipeSecurity.GetSecurityDescriptorBinaryForm(), 0); - - // Create optional security attributes based on provided PipeSecurity. - NamedPipeNative.SECURITY_ATTRIBUTES securityAttributes = null; - GCHandle? securityDescHandle = null; - if (securityDesc != null) - { - byte[] securityDescBuffer = new byte[securityDesc.BinaryLength]; - securityDesc.GetBinaryForm(securityDescBuffer, 0); - - securityDescHandle = GCHandle.Alloc(securityDescBuffer, GCHandleType.Pinned); - securityAttributes = NamedPipeNative.GetSecurityAttributes(securityDescHandle.Value); - } - - // Create named pipe. - SafePipeHandle pipeHandle = NamedPipeNative.CreateNamedPipe( - fullPipeName, - NamedPipeNative.PIPE_ACCESS_DUPLEX | NamedPipeNative.FILE_FLAG_FIRST_PIPE_INSTANCE | NamedPipeNative.FILE_FLAG_OVERLAPPED, - NamedPipeNative.PIPE_TYPE_BYTE | NamedPipeNative.PIPE_READMODE_BYTE, - 1, - 1024, - 1024, - 0, - securityAttributes); - - int lastError = Marshal.GetLastWin32Error(); - if (securityDescHandle != null) - { - securityDescHandle.Value.Free(); - } - - if (pipeHandle.IsInvalid) - { - throw new InvalidOperationException(); - } - // Create the .Net NamedPipeServerStream wrapper. - try - { - return new NamedPipeServerStream( - PipeDirection.InOut, - true, // IsAsync - false, // IsConnected - pipeHandle); - } - catch (Exception) - { - pipeHandle.Dispose(); - throw; - } - } - #endregion - } } diff --git a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs index 5db738b80..a697f161e 100644 --- a/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs +++ b/src/PowerShellEditorServices.Protocol/Server/DebugAdapter.cs @@ -160,13 +160,12 @@ private async Task OnExecutionCompletedAsync(Task executeTask) // Respond to the disconnect request and stop the server await _disconnectRequestContext.SendResultAsync(null); Stop(); + return; } - else - { - await _messageSender.SendEventAsync( - TerminatedEvent.Type, - new TerminatedEvent()); - } + + await _messageSender.SendEventAsync( + TerminatedEvent.Type, + new TerminatedEvent()); } protected void Stop()