Skip to content

EditorServiceHost: allow Tcp/NamedPipe/Stdio listeners #629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Apr 23, 2018
50 changes: 41 additions & 9 deletions module/PowerShellEditorServices/PowerShellEditorServices.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,21 @@ function Start-EditorServicesHost {
[string]
$HostVersion,

[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[int]
$LanguageServicePort,

[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[int]
$DebugServicePort,

[switch]
$Stdio,

[string]
$LanguageServiceNamedPipe,

[string]
$DebugServiceNamedPipe,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the different connection types be separate parameter sets? For example

[Parameter(Mandatory, ParameterSetName='ByTcpPort')]
[ValidateNotNull()]
[int]
$LanguageServicePort,

[Parameter(Mandatory, ParameterSetName='ByTcpPort')]
[ValidateNotNull()]
[int]
$DebugServicePort,

[Parameter(Mandatory, ParameterSetName='ByNamedPipe')]
[ValidateNotNullOrEmpty()]
[string]
$LanguageServiceNamedPipe,

[Parameter(Mandatory, ParameterSetName='ByNamedPipe')]
[ValidateNotNullOrEmpty()]
[string]
$DebugServiceNamePipe,

# etc...


[ValidateNotNullOrEmpty()]
[string]
$BundledModulesPath,
Expand Down Expand Up @@ -89,12 +94,39 @@ function Start-EditorServicesHost {

$editorServicesHost.StartLogging($LogPath, $LogLevel);

if ($DebugServiceOnly.IsPresent) {
$editorServicesHost.StartDebugService($DebugServicePort, $profilePaths, $false);
$languageServiceConfig = New-Object Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportConfig
$debugServiceConfig = 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
}
else {
$editorServicesHost.StartLanguageService($LanguageServicePort, $profilePaths);
$editorServicesHost.StartDebugService($DebugServicePort, $profilePaths, $true);

if ($LanguageServicePort) {
$languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Tcp
$languageServiceConfig.Endpoint = "$LanguageServicePort"
}

if ($DebugServicePort) {
$debugServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Tcp
$debugServiceConfig.Endpoint = "$DebugServicePort"
}

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"
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With parameter sets this would work nicely as a switch statement

switch ($PSCmdlet.ParameterSetName) {
    ByTcp {
        $languageServiceConfig.TransportType = $debugServiceConfig.TransportType =
            Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::Tcp

        $languageServiceConfig.Endpoint = $LanguageServicePort
        $debugServiceConfig.Endpoint = $DebugServicePort
    }
    ByNamedPipe {
        $languageServiceConfig.TransportType = $debugServiceConfig.TransportType =
            [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe

        $languageServiceConfig.Endpoint = $LanguageServiceNamedPipe
        $debugServiceConfig.Endpoint = $DebugServiceNamedPipe
    }
    # etc...
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooo that's nice 😍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with @SeeminglyScience but if you'd prefer we can add the parameter sets after this gets merged.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah this is totally cool!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just thought about this for a while.. A few days ago I was debugging a problem when both language service and debug service are talking to stdio and the channel gets too crowded. In fact these two should never sit on the same stdio train. :)


if ($DebugServiceOnly.IsPresent) {
$editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $false);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a huge thing but the script style for this file doesn't use cuddled else. Again, something we can tweak after merging.

$editorServicesHost.StartLanguageService($languageServiceConfig, $profilePaths);
$editorServicesHost.StartDebugService($debugServiceConfig, $profilePaths, $true);
}

return $editorServicesHost
Expand Down
48 changes: 35 additions & 13 deletions module/Start-EditorServices.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,16 @@ param(
$WaitForDebugger,

[switch]
$ConfirmInstall
$ConfirmInstall,

[switch]
$Stdio,

[string]
$LanguageServicePipeName = $null,

[string]
$DebugServicePipeName = $null
)

$minPortNumber = 10000
Expand Down Expand Up @@ -271,16 +280,21 @@ try {
Import-Module PowerShellEditorServices -Version $parsedVersion -ErrorAction Stop
}

# Locate available port numbers for services
Log "Searching for available socket port for the language service"
$languageServicePort = Get-AvailablePort
# Locate available port numbers for services
# There could be only one service on Stdio channel

Log "Searching for available socket port for the debug service"
$debugServicePort = Get-AvailablePort
$languageServiceTransport = $null
$debugServiceTransport = $null

if (!$languageServicePort -or !$debugServicePort) {
ExitWithError "Failed to find an open socket port for either the language or debug service."
}
if ($Stdio.IsPresent -and -not $DebugServiceOnly.IsPresent) { $languageServiceTransport = "Stdio" }
elseif ($LanguageServicePipeName) { $languageServiceTransport = "NamedPipe"; $languageServicePipeName = "$LanguageServicePipeName" }
elseif ($languageServicePort = Get-AvailablePort) { $languageServiceTransport = "Tcp" }
else { ExitWithError "Failed to find an open socket port for language service." }

if ($Stdio.IsPresent -and $DebugServiceOnly.IsPresent) { $debugServiceTransport = "Stdio" }
elseif ($DebugServicePipeName) { $debugServiceTransport = "NamedPipe"; $debugServicePipeName = "$DebugServicePipeName" }
elseif ($debugServicePort = Get-AvailablePort) { $debugServiceTransport = "Tcp" }
else { ExitWithError "Failed to find an open socket port for debug service." }

if ($EnableConsoleRepl) {
Write-Host "PowerShell Integrated Console`n"
Expand All @@ -298,6 +312,9 @@ try {
-AdditionalModules $AdditionalModules `
-LanguageServicePort $languageServicePort `
-DebugServicePort $debugServicePort `
-LanguageServiceNamedPipe $LanguageServicePipeName `
-DebugServiceNamedPipe $DebugServicePipeName `
-Stdio:$Stdio.IsPresent`
-BundledModulesPath $BundledModulesPath `
-EnableConsoleRepl:$EnableConsoleRepl.IsPresent `
-DebugServiceOnly:$DebugServiceOnly.IsPresent `
Expand All @@ -308,10 +325,15 @@ try {

$resultDetails = @{
"status" = "started";
"channel" = "tcp";
"languageServicePort" = $languageServicePort;
"debugServicePort" = $debugServicePort;
}
"languageServiceTransport" = $languageServiceTransport;
"debugServiceTransport" = $debugServiceTransport;
};

if ($languageServicePipeName) { $resultDetails["languageServicePipeName"] = "$languageServicePipeName" }
if ($debugServicePipeName) { $resultDetails["debugServicePipeName"] = "$debugServicePipeName" }

if ($languageServicePort) { $resultDetails["languageServicePort"] = $languageServicePort }
if ($debugServicePort) { $resultDetails["debugServicePort"] = $debugServicePort }

# Notify the client that the services have started
WriteSessionFile $resultDetails
Expand Down
77 changes: 56 additions & 21 deletions src/PowerShellEditorServices.Host/EditorServicesHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@ public enum EditorServicesHostStatus
Ended
}

public enum EditorServiceTransportType
{
Tcp,
NamedPipe,
Stdio
}

public class EditorServiceTransportConfig
{
public EditorServiceTransportType TransportType { get; set; }
/// <summary>
/// Configures the endpoint of the transport.
/// For Tcp it's an integer specifying the port.
/// For Stdio it's ignored.
/// For NamedPipe it's the pipe name.
/// </summary>
public string Endpoint { get; set; }
}

/// <summary>
/// Provides a simplified interface for hosting the language and debug services
/// over the named pipe server protocol.
Expand All @@ -48,8 +67,8 @@ public class EditorServicesHost
private HashSet<string> featureFlags;
private LanguageServer languageServer;

private TcpSocketServerListener languageServiceListener;
private TcpSocketServerListener debugServiceListener;
private IServerListener languageServiceListener;
private IServerListener debugServiceListener;

private TaskCompletionSource<bool> serverCompletedTask;

Expand Down Expand Up @@ -164,29 +183,25 @@ public void StartLogging(string logFilePath, LogLevel logLevel)
/// </summary>
/// <param name="languageServicePort">The port number for the language service.</param>
/// <param name="profilePaths">The object containing the profile paths to load for this session.</param>
public void StartLanguageService(int languageServicePort, ProfilePaths profilePaths)
public void StartLanguageService(EditorServiceTransportConfig config, ProfilePaths profilePaths)
{
this.profilePaths = profilePaths;

this.languageServiceListener =
new TcpSocketServerListener(
MessageProtocolType.LanguageServer,
languageServicePort,
this.logger);
this.languageServiceListener = CreateServiceListener(MessageProtocolType.LanguageServer, config);

this.languageServiceListener.ClientConnect += this.OnLanguageServiceClientConnect;
this.languageServiceListener.Start();

this.logger.Write(
LogLevel.Normal,
string.Format(
"Language service started, listening on port {0}",
languageServicePort));
"Language service started, type = {0}, endpoint = {1}",
config.TransportType, config.Endpoint));
}

private async void OnLanguageServiceClientConnect(
object sender,
TcpSocketServerChannel serverChannel)
ChannelBase serverChannel)
{
MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger);

Expand Down Expand Up @@ -238,27 +253,22 @@ await this.editorSession.PowerShellContext.ImportCommandsModule(
/// </summary>
/// <param name="debugServicePort">The port number for the debug service.</param>
public void StartDebugService(
int debugServicePort,
EditorServiceTransportConfig config,
ProfilePaths profilePaths,
bool useExistingSession)
{
this.debugServiceListener =
new TcpSocketServerListener(
MessageProtocolType.DebugAdapter,
debugServicePort,
this.logger);

this.debugServiceListener = CreateServiceListener(MessageProtocolType.DebugAdapter, config);
this.debugServiceListener.ClientConnect += OnDebugServiceClientConnect;
this.debugServiceListener.Start();

this.logger.Write(
LogLevel.Normal,
string.Format(
"Debug service started, listening on port {0}",
debugServicePort));
"Debug service started, type = {0}, endpoint = {1}",
config.TransportType, config.Endpoint));
}

private void OnDebugServiceClientConnect(object sender, TcpSocketServerChannel serverChannel)
private void OnDebugServiceClientConnect(object sender, ChannelBase serverChannel)
{
MessageDispatcher messageDispatcher = new MessageDispatcher(this.logger);

Expand Down Expand Up @@ -441,6 +451,31 @@ private void CurrentDomain_UnhandledException(
e.ExceptionObject.ToString()));
}
#endif
private IServerListener CreateServiceListener(MessageProtocolType protocol, EditorServiceTransportConfig config)
{
switch (config.TransportType)
{
case EditorServiceTransportType.Tcp:
{
return new TcpSocketServerListener(protocol, int.Parse(config.Endpoint), this.logger);
}

case EditorServiceTransportType.Stdio:
{
return new StdioServerListener(protocol, this.logger);
}

case EditorServiceTransportType.NamedPipe:
{
return new NamedPipeServerListener(protocol, config.Endpoint, this.logger);
}

default:
{
throw new NotSupportedException();
}
}
}

#endregion
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public override void Start()
1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);
ListenForConnection();
}
catch (IOException e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
{
public abstract class ServerListenerBase<TChannel>
public abstract class ServerListenerBase<TChannel> : IServerListener
where TChannel : ChannelBase
{
private MessageProtocolType messageProtocolType;
Expand All @@ -22,12 +22,21 @@ public ServerListenerBase(MessageProtocolType messageProtocolType)

public abstract void Stop();

public event EventHandler<TChannel> ClientConnect;
public event EventHandler<ChannelBase> ClientConnect;

protected void OnClientConnect(TChannel channel)
{
channel.Start(this.messageProtocolType);
this.ClientConnect?.Invoke(this, channel);
}
}

public interface IServerListener
{
void Start();

void Stop();

event EventHandler<ChannelBase> ClientConnect;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1362,13 +1362,16 @@ private static FileChange GetFileChangeDetails(Range changeRange, string insertS
{
// The protocol's positions are zero-based so add 1 to all offsets

if (changeRange == null) return new FileChange { InsertString = insertString, IsReload = true };

return new FileChange
{
InsertString = insertString,
Line = changeRange.Start.Line + 1,
Offset = changeRange.Start.Character + 1,
EndLine = changeRange.End.Line + 1,
EndOffset = changeRange.End.Character + 1
EndOffset = changeRange.End.Character + 1,
IsReload = false
};
}

Expand Down
7 changes: 7 additions & 0 deletions src/PowerShellEditorServices/Workspace/FileChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,12 @@ public class FileChange
/// The 1-based column offset where the change ends.
/// </summary>
public int EndOffset { get; set; }

/// <summary>
/// Indicates that the InsertString is an overwrite
/// of the content, and all stale content and metadata
/// should be discarded.
/// </summary>
public bool IsReload { get; set; }
}
}
Loading