diff --git a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 index 956287f2b..dd8f463a6 100644 --- a/module/PowerShellEditorServices/PowerShellEditorServices.psm1 +++ b/module/PowerShellEditorServices/PowerShellEditorServices.psm1 @@ -31,15 +31,37 @@ function Start-EditorServicesHost { [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, @@ -99,19 +121,32 @@ function Start-EditorServicesHost { $debugServiceConfig = Microsoft.PowerShell.Utility\New-Object Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportConfig - if ($Stdio.IsPresent) { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Stdio - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Stdio - } - - if ($LanguageServiceNamedPipe) { - $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe - $languageServiceConfig.Endpoint = "$LanguageServiceNamedPipe" - } - - if ($DebugServiceNamedPipe) { - $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe - $debugServiceConfig.Endpoint = "$DebugServiceNamedPipe" + switch ($PSCmdlet.ParameterSetName) { + "Stdio" { + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Stdio + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Stdio + break + } + "NamedPipe" { + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe + $languageServiceConfig.InOutPipeName = "$LanguageServiceNamedPipe" + if ($DebugServiceNamedPipe) { + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe + $debugServiceConfig.InOutPipeName = "$DebugServiceNamedPipe" + } + break + } + "NamedPipeSimplex" { + $languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe + $languageServiceConfig.InPipeName = $LanguageServiceInNamedPipe + $languageServiceConfig.OutPipeName = $LanguageServiceOutNamedPipe + if ($DebugServiceInNamedPipe -and $DebugServiceOutNamedPipe) { + $debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe + $debugServiceConfig.InPipeName = $DebugServiceInNamedPipe + $debugServiceConfig.OutPipeName = $DebugServiceOutNamedPipe + } + break + } } if ($DebugServiceOnly.IsPresent) { diff --git a/module/PowerShellEditorServices/Start-EditorServices.ps1 b/module/PowerShellEditorServices/Start-EditorServices.ps1 index 9d3970adf..79e2db18c 100644 --- a/module/PowerShellEditorServices/Start-EditorServices.ps1 +++ b/module/PowerShellEditorServices/Start-EditorServices.ps1 @@ -15,7 +15,7 @@ # Services GitHub repository: # # https://github.com/PowerShell/PowerShellEditorServices/blob/master/module/PowerShellEditorServices/Start-EditorServices.ps1 - +[CmdletBinding(DefaultParameterSetName="NamedPipe")] param( [Parameter(Mandatory=$true)] [ValidateNotNullOrEmpty()] @@ -65,14 +65,37 @@ param( [switch] $ConfirmInstall, + [Parameter(ParameterSetName="Stdio",Mandatory=$true)] [switch] $Stdio, + [Parameter(ParameterSetName="NamedPipe")] [string] $LanguageServicePipeName = $null, + [Parameter(ParameterSetName="NamedPipe")] + [string] + $DebugServicePipeName = $null, + + [Parameter(ParameterSetName="NamedPipeSimplex")] + [switch] + $SplitInOutPipes, + + [Parameter(ParameterSetName="NamedPipeSimplex")] + [string] + $LanguageServiceInPipeName, + + [Parameter(ParameterSetName="NamedPipeSimplex")] [string] - $DebugServicePipeName = $null + $LanguageServiceOutPipeName, + + [Parameter(ParameterSetName="NamedPipeSimplex")] + [string] + $DebugServiceInPipeName = $null, + + [Parameter(ParameterSetName="NamedPipeSimplex")] + [string] + $DebugServiceOutPipeName = $null ) $DEFAULT_USER_MODE = "600" @@ -173,8 +196,7 @@ function New-NamedPipeName { # We try 10 times to find a valid pipe name for ($i = 0; $i -lt 10; $i++) { - # add a guid to make the pipe unique - $PipeName = "PSES_$([guid]::NewGuid())" + $PipeName = "PSES_$([System.IO.Path]::GetRandomFileName())" if ((Test-NamedPipeName -PipeName $PipeName)) { return $PipeName @@ -246,6 +268,37 @@ function Set-NamedPipeMode { LogSection "Console Encoding" Log $OutputEncoding +function Test-NamedPipeName-OrCreate-IfNull { + param( + [string] + $PipeName + ) + if (-not $PipeName) { + $PipeName = New-NamedPipeName + } + else { + if (-not (Test-NamedPipeName -PipeName $PipeName)) { + ExitWithError "Pipe name supplied is already taken: $PipeName" + } + } + return $PipeName +} + +function Set-PipeFileResult { + param ( + [Hashtable] + $ResultTable, + [string] + $PipeNameKey, + [string] + $PipeNameValue + ) + $ResultTable[$PipeNameKey] = Get-NamedPipePath -PipeName $PipeNameValue + if ($IsLinux -or $IsMacOS) { + Set-NamedPipeMode -PipeFile $ResultTable[$PipeNameKey] + } +} + # Add BundledModulesPath to $env:PSModulePath if ($BundledModulesPath) { $env:PSModulePath = $env:PSModulePath.TrimEnd([System.IO.Path]::PathSeparator) + [System.IO.Path]::PathSeparator + $BundledModulesPath @@ -266,81 +319,93 @@ try { Microsoft.PowerShell.Core\Import-Module PowerShellEditorServices -ErrorAction Stop - # Locate available port numbers for services - # There could be only one service on Stdio channel - - $languageServiceTransport = $null - $debugServiceTransport = $null - - if ($Stdio.IsPresent) { - $languageServiceTransport = "Stdio" - $debugServiceTransport = "Stdio" - } - else { - $languageServiceTransport = "NamedPipe" - $debugServiceTransport = "NamedPipe" - if (-not $LanguageServicePipeName) { - $LanguageServicePipeName = New-NamedPipeName - } - else { - if (-not (Test-NamedPipeName -PipeName $LanguageServicePipeName)) { - ExitWithError "Pipe name supplied is already taken: $LanguageServicePipeName" - } - } - if (-not $DebugServicePipeName) { - $DebugServicePipeName = New-NamedPipeName - } - else { - if (-not (Test-NamedPipeName -PipeName $DebugServicePipeName)) { - ExitWithError "Pipe name supplied is already taken: $DebugServicePipeName" - } - } - } - if ($EnableConsoleRepl) { Write-Host "PowerShell Integrated Console`n" } - # Create the Editor Services host - Log "Invoking Start-EditorServicesHost" - $editorServicesHost = - Start-EditorServicesHost ` - -HostName $HostName ` - -HostProfileId $HostProfileId ` - -HostVersion $HostVersion ` - -LogPath $LogPath ` - -LogLevel $LogLevel ` - -AdditionalModules $AdditionalModules ` - -LanguageServiceNamedPipe $LanguageServicePipeName ` - -DebugServiceNamedPipe $DebugServicePipeName ` - -Stdio:$Stdio.IsPresent` - -BundledModulesPath $BundledModulesPath ` - -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` - -DebugServiceOnly:$DebugServiceOnly.IsPresent ` - -WaitForDebugger:$WaitForDebugger.IsPresent - - # TODO: Verify that the service is started - Log "Start-EditorServicesHost returned $editorServicesHost" - $resultDetails = @{ - "status" = "started"; - "languageServiceTransport" = $languageServiceTransport; - "debugServiceTransport" = $debugServiceTransport; + "status" = "not started"; + "languageServiceTransport" = $PSCmdlet.ParameterSetName; + "debugServiceTransport" = $PSCmdlet.ParameterSetName; }; - if ($LanguageServicePipeName) { - $resultDetails["languageServicePipeName"] = Get-NamedPipePath -PipeName $LanguageServicePipeName - if ($IsLinux -or $IsMacOS) { - Set-NamedPipeMode -PipeFile $resultDetails["languageServicePipeName"] + # 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 + break } - } - if ($DebugServicePipeName) { - $resultDetails["debugServicePipeName"] = Get-NamedPipePath -PipeName $DebugServicePipeName - if ($IsLinux -or $IsMacOS) { - Set-NamedPipeMode -PipeFile $resultDetails["debugServicePipeName"] + "NamedPipeSimplex" { + $LanguageServiceInPipeName = Test-NamedPipeName-OrCreate-IfNull $LanguageServiceInPipeName + $LanguageServiceOutPipeName = Test-NamedPipeName-OrCreate-IfNull $LanguageServiceOutPipeName + $DebugServiceInPipeName = Test-NamedPipeName-OrCreate-IfNull $DebugServiceInPipeName + $DebugServiceOutPipeName = Test-NamedPipeName-OrCreate-IfNull $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 ` + -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` + -DebugServiceOnly:$DebugServiceOnly.IsPresent ` + -WaitForDebugger:$WaitForDebugger.IsPresent + + Set-PipeFileResult $resultDetails "languageServiceReadPipeName" $LanguageServiceInPipeName + Set-PipeFileResult $resultDetails "languageServiceWritePipeName" $LanguageServiceOutPipeName + Set-PipeFileResult $resultDetails "debugServiceReadPipeName" $DebugServiceInPipeName + Set-PipeFileResult $resultDetails "debugServiceWritePipeName" $DebugServiceOutPipeName + break + } + Default { + $LanguageServicePipeName = Test-NamedPipeName-OrCreate-IfNull $LanguageServicePipeName + $DebugServicePipeName = Test-NamedPipeName-OrCreate-IfNull $DebugServicePipeName + + $editorServicesHost = Start-EditorServicesHost ` + -HostName $HostName ` + -HostProfileId $HostProfileId ` + -HostVersion $HostVersion ` + -LogPath $LogPath ` + -LogLevel $LogLevel ` + -AdditionalModules $AdditionalModules ` + -LanguageServiceNamedPipe $LanguageServicePipeName ` + -DebugServiceNamedPipe $DebugServicePipeName ` + -BundledModulesPath $BundledModulesPath ` + -EnableConsoleRepl:$EnableConsoleRepl.IsPresent ` + -DebugServiceOnly:$DebugServiceOnly.IsPresent ` + -WaitForDebugger:$WaitForDebugger.IsPresent + + 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 diff --git a/src/PowerShellEditorServices.Host/EditorServicesHost.cs b/src/PowerShellEditorServices.Host/EditorServicesHost.cs index 0d1f7e8e4..f30ffc4ec 100644 --- a/src/PowerShellEditorServices.Host/EditorServicesHost.cs +++ b/src/PowerShellEditorServices.Host/EditorServicesHost.cs @@ -44,7 +44,10 @@ public class EditorServiceTransportConfig /// For Stdio it's ignored. /// For NamedPipe it's the pipe name. /// - public string Endpoint { get; set; } + 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}"; } /// @@ -463,7 +466,15 @@ private IServerListener CreateServiceListener(MessageProtocolType protocol, Edit case EditorServiceTransportType.NamedPipe: { - return new NamedPipeServerListener(protocol, config.Endpoint, this.logger); + if (config.OutPipeName !=null && config.InPipeName !=null) + { + this.logger.Write(LogLevel.Verbose, $"Creating NamedPipeServerListener for ${protocol} protocol with two pipes: In: '{config.InPipeName}'. Out: '{config.OutPipeName}'"); + return new NamedPipeServerListener(protocol, config.InPipeName, config.OutPipeName, this.logger); + } + else + { + return new NamedPipeServerListener(protocol, config.InOutPipeName, this.logger); + } } default: diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs index ad4e01de9..870640c49 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerChannel.cs @@ -11,13 +11,23 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel public class NamedPipeServerChannel : ChannelBase { private ILogger logger; - private NamedPipeServerStream pipeServer; + private NamedPipeServerStream inOutPipeServer; + private NamedPipeServerStream outPipeServer; public NamedPipeServerChannel( - NamedPipeServerStream pipeServer, + NamedPipeServerStream inOutPipeServer, ILogger logger) { - this.pipeServer = pipeServer; + this.inOutPipeServer = inOutPipeServer; + this.logger = logger; + } + public NamedPipeServerChannel( + NamedPipeServerStream inOutPipeServer, + NamedPipeServerStream outPipeServer, + ILogger logger) + { + this.inOutPipeServer = inOutPipeServer; + this.outPipeServer = outPipeServer; this.logger = logger; } @@ -25,13 +35,13 @@ protected override void Initialize(IMessageSerializer messageSerializer) { this.MessageReader = new MessageReader( - this.pipeServer, + this.inOutPipeServer, messageSerializer, this.logger); this.MessageWriter = new MessageWriter( - this.pipeServer, + this.outPipeServer ?? this.inOutPipeServer, messageSerializer, this.logger); } @@ -39,7 +49,8 @@ protected override void Initialize(IMessageSerializer messageSerializer) protected override void Shutdown() { // The server listener will take care of the pipe server - this.pipeServer = null; + this.inOutPipeServer = null; + this.outPipeServer = null; } } } diff --git a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs index 44d6acb75..ba2c74931 100644 --- a/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs +++ b/src/PowerShellEditorServices.Protocol/MessageProtocol/Channel/NamedPipeServerListener.cs @@ -6,6 +6,7 @@ using Microsoft.PowerShell.EditorServices.Utility; using Microsoft.Win32.SafeHandles; using System; +using System.Collections.Generic; using System.IO; using System.IO.Pipes; using System.Runtime.InteropServices; @@ -18,17 +19,31 @@ namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel public class NamedPipeServerListener : ServerListenerBase { private ILogger logger; - private string pipeName; - private NamedPipeServerStream pipeServer; + private string inOutPipeName; + private readonly string outPipeName; + private NamedPipeServerStream inOutPipeServer; + private NamedPipeServerStream outPipeServer; public NamedPipeServerListener( MessageProtocolType messageProtocolType, - string pipeName, + string inOutPipeName, + ILogger logger) + : base(messageProtocolType) + { + this.logger = logger; + this.inOutPipeName = inOutPipeName; + } + + public NamedPipeServerListener( + MessageProtocolType messageProtocolType, + string inPipeName, + string outPipeName, ILogger logger) : base(messageProtocolType) { this.logger = logger; - this.pipeName = pipeName; + this.inOutPipeName = inPipeName; + this.outPipeName = outPipeName; } public override void Start() @@ -62,18 +77,31 @@ public override void Start() // 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.pipeServer = NamedPipeNative.CreateNamedPipe(pipeName, pipeSecurity); + this.inOutPipeServer = NamedPipeNative.CreateNamedPipe(inOutPipeName, pipeSecurity); + if (this.outPipeName != null) + { + this.outPipeServer = NamedPipeNative.CreateNamedPipe(outPipeName, pipeSecurity); + } } else { // This handles the Unix case since PipeSecurity is not supported on Unix. // Instead, we use chmod in Start-EditorServices.ps1 - this.pipeServer = new NamedPipeServerStream( - pipeName: pipeName, + this.inOutPipeServer = new NamedPipeServerStream( + pipeName: inOutPipeName, direction: PipeDirection.InOut, maxNumberOfServerInstances: 1, transmissionMode: PipeTransmissionMode.Byte, options: PipeOptions.Asynchronous); + if (this.outPipeName != null) + { + this.outPipeServer = new NamedPipeServerStream( + pipeName: outPipeName, + direction: PipeDirection.Out, + maxNumberOfServerInstances: 1, + transmissionMode: PipeTransmissionMode.Byte, + options: PipeOptions.None); + } } ListenForConnection(); } @@ -89,47 +117,58 @@ public override void Start() public override void Stop() { - if (this.pipeServer != null) + if (this.inOutPipeServer != null) { this.logger.Write(LogLevel.Verbose, "Named pipe server shutting down..."); - this.pipeServer.Dispose(); + this.inOutPipeServer.Dispose(); this.logger.Write(LogLevel.Verbose, "Named pipe server has been disposed."); } + if (this.outPipeServer != null) + { + this.logger.Write(LogLevel.Verbose, $"Named out pipe server {outPipeServer} shutting down..."); + + this.outPipeServer.Dispose(); + + this.logger.Write(LogLevel.Verbose, $"Named out pipe server {outPipeServer} has been disposed."); + } } private void ListenForConnection() { - Task.Factory.StartNew( - async () => + var connectionTasks = new List {WaitForConnectionAsync(this.inOutPipeServer)}; + if (this.outPipeServer != null) + { + connectionTasks.Add(WaitForConnectionAsync(this.outPipeServer)); + } + + Task.Run(async () => + { + try { - try - { + await Task.WhenAll(connectionTasks); + this.OnClientConnect(new NamedPipeServerChannel(this.inOutPipeServer, this.outPipeServer, this.logger)); + } + catch (Exception e) + { + this.logger.WriteException( + "An unhandled exception occurred while listening for a named pipe client connection", + e); + + throw; + } + }); + } + + private static async Task WaitForConnectionAsync(NamedPipeServerStream pipeServerStream) + { #if CoreCLR - await this.pipeServer.WaitForConnectionAsync(); + await pipeServerStream.WaitForConnectionAsync(); #else - await Task.Factory.FromAsync( - this.pipeServer.BeginWaitForConnection, - this.pipeServer.EndWaitForConnection, null); + await Task.Factory.FromAsync(pipeServerStream.BeginWaitForConnection, pipeServerStream.EndWaitForConnection, null); #endif - - await this.pipeServer.FlushAsync(); - - this.OnClientConnect( - new NamedPipeServerChannel( - this.pipeServer, - this.logger)); - } - catch (Exception e) - { - this.logger.WriteException( - "An unhandled exception occurred while listening for a named pipe client connection", - e); - - throw e; - } - }); + await pipeServerStream.FlushAsync(); } }