-
Notifications
You must be signed in to change notification settings - Fork 223
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
Adds ability to use separate pipes for reading and writing #785
Conversation
Some clients (e.g. Java on Windows) have blocking File I/O API, thus it is not possible for them to use single named pipe for reading and writing asynchronously. It will add the ability to use separate pipes for reading and writing when the client specifies the "-SplitInOutPipes" switch for the startup script.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really good! Just a couple questions.
I'm really excited to get this in and have another editor that uses the latest version of PSES 😄
{ | ||
this.writePipeServer = new NamedPipeServerStream( | ||
pipeName: writePipeName, | ||
direction: PipeDirection.InOut, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the direction here be just PipeDirection.In
?
@@ -463,7 +463,19 @@ private IServerListener CreateServiceListener(MessageProtocolType protocol, Edit | |||
|
|||
case EditorServiceTransportType.NamedPipe: | |||
{ | |||
return new NamedPipeServerListener(protocol, config.Endpoint, this.logger); | |||
string endpoint = config.Endpoint; | |||
int splitIndex = endpoint.IndexOf(';'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm more inclined to say that the character that separates the two file names should be the DirectorySeparator
(System.IO.Path.DirectorySeparatorChar
) since ;
could be a part of a valid file name. I don't think GetRandomFileName
would ever spit out a ;
but at least if we use the DirectorySeparatorChar
we can be sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I spoke to @rjmholt about the use of Endpoint
here. Historically, this was used to support TCP and NamedPipes but since we don't have TCP anymore, we could change this EditorServiceTransportConfig
to have a Pipe
and WritePipe
property (but named better) that way you don't have to do this string splitting.
I think I'm ok if you don't want to do this in this PR - but it's pretty important so if you want to take it on I'd really appreciate it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this work @ant-druha.
I've left a couple of comments, but they aren't really blocking.
The big thing is that our start script and configuration are starting to get pretty complex and I think we (tagging @tylerl0706, @SeeminglyScience, @rkeithhill) should discuss a cleaner way to pass configuration in and start EditorServices, specifically what it should look like and what breaking changes it would involve. Just because it strikes me that the combinatorics of all the configuration we're trying to juggle now are starting to get spaghettified, and with v2 on the horizon and more clients wanting to depend on us, we have an opportunity to simplify it all.
But all good for this PR.
@@ -106,12 +112,24 @@ function Start-EditorServicesHost { | |||
|
|||
if ($LanguageServiceNamedPipe) { | |||
$languageServiceConfig.TransportType = [Microsoft.PowerShell.EditorServices.Host.EditorServiceTransportType]::NamedPipe | |||
$languageServiceConfig.Endpoint = "$LanguageServiceNamedPipe" | |||
if ($LanguageServiceWriteNamedPipe) { | |||
$languageServiceConfig.Endpoint = "$LanguageServiceNamedPipe;$LanguageServiceWriteNamedPipe" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This way of specifying two pipe names in a single string here works, but I'm not a huge fan of it.
Getting around it without a breaking change doesn't seem all that simple, but I think we should revisit this soon. Otherwise all our config options are going to end up named something abstract and needing a bunch of preprocessing to unspool, even though we have a typed, in-memory .NET API.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not suggesting this be changed in any way other than @tylerl0706's suggestion, but I don't think this should be the long term solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps something to re-visit for v2?
@@ -269,6 +271,9 @@ try { | |||
$languageServiceTransport = $null | |||
$debugServiceTransport = $null | |||
|
|||
$LanguageServiceWritePipeName = $null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are these lines here needed? Or would leaving them out have the same effect? Since they are PascalCased, I'm assuming they are script parameters, so the nulling could be done in the parameter definition if required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a script parameter. This variable is a local so it should be camelCased - $languageServiceWritePipeName
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto for $DebugServiceWritePipeName
below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, missed this. Will make them as script parameters and move to a separate parameter set.
if ($IsLinux -or $IsMacOS) { | ||
Set-NamedPipeMode -PipeFile $resultDetails["languageServicePipeName"] | ||
if ($LanguageServiceWritePipeName) { | ||
$resultDetails["languageServiceReadPipeName"] = Get-NamedPipePath -PipeName $LanguageServicePipeName |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this pattern is repeated 3 times. Would it be possible to make it a function?
} | ||
|
||
private void ListenForConnection() | ||
{ | ||
Task.Factory.StartNew( | ||
async () => | ||
List<Task> connectionTasks = new List<Task> {WaitForConnectionAsync(this.pipeServer)}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can use var
here on the LHS instead of List<Task>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used var
at first :) but having a look at other code here thought it is a coding style to use explicit types (including local vars). Can make it var
if you'd like.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yeah the coding style in this project isn't great at the moment -- we've been trying to move it over gradually to being less redundant and verbose.
@@ -106,12 +112,24 @@ function Start-EditorServicesHost { | |||
|
|||
if ($LanguageServiceNamedPipe) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we used parameter sets then here we'd check if ($PSCmdlet.ParameterSetName -eq 'NamedPipeDuplex') and perhaps $LanguageServicePipe would be a mandatory parameter? Not sure about $DebugLanguageServicePipe, I suppose you could create one but not the other?
… transport config endpoint
So, I have organised startup script parameters into parameter sets (had to move code around a bit for this, sorry). Also introduced separate properties for pipes in I hope it is suffice for this PR to make it more or less better. Looking forward to a new release :) |
[string] | ||
$DebugServiceNamedPipe, | ||
|
||
[Parameter(ParameterSetName="NamedPipeHalfDuplex",Mandatory=$true)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, I think this may be "simplex" rather than "half duplex" since each pipe can only ever be used in one direction (rather than both directions, one at a time), but it's not a major concern
[string] | ||
$DebugServicePipeName = $null, | ||
|
||
[Parameter(ParameterSetName="NamedPipeHalfDuplex",Mandatory=$true)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting Mandatory=$true
for a switch parameter may be unneeded, particularly for one in its own parameter set.
[switch] | ||
$SplitInOutPipes, | ||
|
||
[Parameter(ParameterSetName="NamedPipeHalfDuplexParam",Mandatory=$true)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reason for the "Param"
parameter set? Is it so that you can either set a switch to split the pipe or nominate a pipename yourself?
I feel like we probably don't need so many different parameter sets -- what is the envisioned usage of each current set?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the reason for the
"Param"
parameter set? Is it so that you can either set a switch to split the pipe or nominate a pipename yourself?I feel like we probably don't need so many different parameter sets -- what is the envisioned usage of each current set?
Right. I wanted to enforce the contract that one could either specify SplitInOutPipes
Switch or be enforced to specify read/write pipe names directory. Ok, simplified the parameter sets to just one additional set for simplex named pipes.
return $PipeName | ||
} | ||
|
||
function SetPipeFileResult($ResultTable, [string]$PipeNameKey, [string]$PipeNameValue) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function should separate the verb Set
with a dash in the name, and also use a param
block inside the body rather than take parameters after the function name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If result table is expected to be a hashtable, it might be worth adding that to the param signature as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Thanks @ant-druha!
Just a few optional nit picks and suggestions.
@@ -15,7 +15,7 @@ | |||
# Services GitHub repository: | |||
# | |||
# https://github.com/PowerShell/PowerShellEditorServices/blob/master/module/PowerShellEditorServices/Start-EditorServices.ps1 | |||
|
|||
[Cmdletbinding(DefaultParameterSetName="NamedPipe")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Cmdletbinding(DefaultParameterSetName="NamedPipe")] | |
[CmdletBinding(DefaultParameterSetName="NamedPipe")] |
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}"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
internal string Endpoint => OutPipeName != null && InPipeName!=null ? $"In pipe: {InPipeName} Out pipe: {OutPipeName}" : $" InOut pipe: {InOutPipeName}"; | |
internal string Endpoint => OutPipeName != null && InPipeName != null ? $"In pipe: {InPipeName} Out pipe: {OutPipeName}" : $" InOut pipe: {InOutPipeName}"; |
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 + "'"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.logger.Write(LogLevel.Verbose, $"Creating NamedPipeServerListener for ${protocol} protocol with two pipes: In: '" + config.InPipeName + "'. Out: '" + config.OutPipeName + "'"); | |
this.logger.Write(LogLevel.Verbose, $"Creating NamedPipeServerListener for ${protocol} protocol with two pipes: In: '{config.InPipeName}'. Out: '{config.OutPipeName}'"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the $
supposed to be there before {protocol}
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There could be $DebugAdapter or $LanguageServer protocols. Example log:
Creating NamedPipeServerListener for $LanguageServer protocol with two pipes: In: 'PSES_saem21o3.ere'. Out: 'PSES_dmxrnado.rh5'
...
Creating NamedPipeServerListener for $DebugAdapter protocol with two pipes: In: 'PSES_jzzah5uk.4ek'. Out: 'PSES_w3ablqxx.uyb'
ILogger logger) | ||
{ | ||
this.pipeServer = pipeServer; | ||
this.inOutPipeServer = inOutPipeServer; | ||
this.outPipeServer = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: This line can be removed, the field will already be initialized with null
by default.
{ | ||
this.logger = logger; | ||
this.inOutPipeName = inOutPipeName; | ||
this.outPipeName = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: re: field initialization not needed
@ant-druha do you think you'll be able to address this feedback? I just want to make sure this PR doesn't get lost because it's super important! |
Hi @TylerLeonhardt, sorry for the silence! Haven't had time to look at it, yet. Yes I will address the feedback this week or weekend, I hope it is okay. |
# Conflicts: # module/PowerShellEditorServices/Start-EditorServices.ps1
$PipeName = New-NamedPipeName | ||
} | ||
else { | ||
if (-not (Test-NamedPipeName -PipeName $PipeName)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So what happens when Test-NamedPipeName equals true
? Will it return null
? Should that ever happen?
I feel as if the Test & Create part should be two separate functions. Separation of concerns
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function will fail and abort with the error if the supplied by the user pipe name is not a valid name. If a valid name was supplied it just returns this pipe name. If no pipe name was provided it creates it.
"An unhandled exception occurred while listening for a named pipe client connection", | ||
e); | ||
|
||
throw e; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rethrow should never specify the exception - you'll lose stack info. Use throw;
by itself.
Ideally our logger methods would return false so you could use C# exception filters e.g.:
catch (Exception e) when (this.logger.WriteException("...", e))
{
}
But we don't do that - yet. @rjmholt Any chance we could modify the log methods to return false
for this reason?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we could definitely modify the methods.
Or else write wrapper methods that explain the intended caller context:
catch (Exception e) when (_logger.WriteExceptionWithoutUnwinding("...", e))
{
}
Bit of a mouthful -- just some name that says "I return false for use with exception filters".
Hi all. Can this PR be merged? Are there any blockers? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, I removed the 'e' per @rkeithhill's recommendation
Once @rkeithhill is good with this, we can merge it in |
@ant-druha all merged! Let me know when your extension is updated with this. I'd love to spread the word! |
Some clients have blocking File I/O API, thus it is not possible for them to use single named pipe for reading and writing asynchronously.
E.g. this is needed for my PowerShell plugin for IntelliJ IDEs.
For communication protocol I'm using lsp4j library which has no issues with tcp protocol but it does not support named pipes. So the proposed solution was to use two separate pipes.
This request adds the ability to use separate pipes for reading and writing when the client specifies the "-SplitInOutPipes" switch for the startup script.
Tested that two pipes approach with my plugin (on OS X and Win7) works fine. Also tested with VS Code on OS X and Win7 also seems to work fine.