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,

[bool]
$Stdio,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we make this parameter a [switch] parameter instead of [bool]?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, might be better to change name to UseStdio.

Copy link
Author

Choose a reason for hiding this comment

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

@SeeminglyScience I tried to make it a switch as the outer starting script does, but I don't know how to relay the switch in without splitting the command into two, one with the switch and one without. Sorry I'm still learning my way on powershell 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

Typically it is -InnerSwitch:$OuterSwitch


[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) {
$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
29 changes: 24 additions & 5 deletions module/Start-EditorServices.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,19 @@ param(
$WaitForDebugger,

[switch]
$ConfirmInstall
$ConfirmInstall,

[switch]
$Stdio,

[switch]
$DebugServiceOnly,

[string]
$LanguageServicePipeName = $null,

[string]
$DebugServicePipeName = $null
)

# This variable will be assigned later to contain information about
Expand Down Expand Up @@ -123,7 +135,7 @@ function Get-AvailablePort {

# Add BundledModulesPath to $env:PSModulePath
if ($BundledModulesPath) {
$env:PSMODULEPATH = $BundledModulesPath + [System.IO.Path]::PathSeparator + $env:PSMODULEPATH
$env:PsModulePath = $BundledModulesPath + [System.IO.Path]::PathSeparator + $env:PsModulePath
Copy link
Contributor

@rkeithhill rkeithhill Mar 20, 2018

Choose a reason for hiding this comment

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

Close - the s is capitalized. Turns out this is important on *nix platforms e.g.:

PSModulePath                   /home/hillr/.local/share/powershell/Modules:/usr/local/share/powershell/Modules:/opt/...

Case-sensitive env vars & file-system on Linux drives me bonkers. :-)

Copy link
Member

Choose a reason for hiding this comment

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

I think this is the only comment left to be addressed, (sorry for adding this on but I really appreciate it @yatli!)

Fix this and this is totally ready to be merged.

Optional: make sure vscode still works 😄

Copy link
Author

Choose a reason for hiding this comment

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

@tylerl0706 I'm in a deep profiling session right now so I just quickly edited the file with Github online editor. Will be able to do heavier stuff when I get back home :)

}

# Check if PowerShellGet module is available
Expand All @@ -149,8 +161,9 @@ if ((Test-ModuleAvailable "PowerShellEditorServices" -RequiredVersion $parsedVer
Import-Module PowerShellEditorServices -RequiredVersion $parsedVersion -ErrorAction Stop

# Locate available port numbers for services
$languageServicePort = Get-AvailablePort
$debugServicePort = Get-AvailablePort
# There could be only one service on Stdio channel
if (-not (($Stdio.IsPresent -and -not $DebugServiceOnly.IsPresent) -or $LanguageServicePipeName)) { $languageServicePort = Get-AvailablePort }
if (-not (($Stdio.IsPresent -and $DebugServiceOnly.IsPresent) -or $DebugServicePipeName)) { $debugServicePort = Get-AvailablePort }

$editorServicesHost =
Start-EditorServicesHost `
Expand All @@ -162,16 +175,22 @@ $editorServicesHost =
-AdditionalModules @() `
-LanguageServicePort $languageServicePort `
-DebugServicePort $debugServicePort `
-Stdio $Stdio.IsPresent`
-LanguageServiceNamedPipe $LanguageServicePipeName `
-DebugServiceNamedPipe $DebugServicePipeName `
-BundledModulesPath $BundledModulesPath `
-DebugServiceOnly:$DebugServiceOnly.IsPresent`
-WaitForDebugger:$WaitForDebugger.IsPresent
Copy link
Collaborator

Choose a reason for hiding this comment

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

Assuming the switch to parameter sets, this would need to be switched to a splat (probably should anyway to fit common community style guides)

$splat = @{
    AdditionalModules = @()
    WaitForDebugger = $WaitForDebugger.IsPresent
   # etc...
}

switch ($PSCmdlet.ParameterSetName) {
    ByTcp {
        $splat.LanguageServicePort = $languageServicePort
        # etc...
    }
}

$editorServicesHost = Start-EditorServicesHost @splat


# TODO: Verify that the service is started

$resultDetails = @{
"status" = "started";
"channel" = "tcp";
"languageServicePort" = $languageServicePort;
"debugServicePort" = $debugServicePort;
"languageServiceNamedPipe" = $LanguageServicePipeName;
"debugServiceNamedPipe" = $DebugServicePipeName;
"Stdio" = $Stdio.IsPresent;
};

# Notify the client that the services have started
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 @@ -1348,13 +1348,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