Skip to content
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

Allow multiple instances of the server on a single process. #51

Merged
merged 2 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 16 additions & 12 deletions src/LanguageServerProtocol.fs
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,21 @@ module Server =

let logger = LogProvider.getLoggerByName "LSP Server"

let jsonRpcFormatter = new JsonMessageFormatter()
jsonRpcFormatter.JsonSerializer.NullValueHandling <- NullValueHandling.Ignore
jsonRpcFormatter.JsonSerializer.ConstructorHandling <- ConstructorHandling.AllowNonPublicDefaultConstructor
jsonRpcFormatter.JsonSerializer.MissingMemberHandling <- MissingMemberHandling.Ignore
jsonRpcFormatter.JsonSerializer.Converters.Add(StrictNumberConverter())
jsonRpcFormatter.JsonSerializer.Converters.Add(StrictStringConverter())
jsonRpcFormatter.JsonSerializer.Converters.Add(StrictBoolConverter())
jsonRpcFormatter.JsonSerializer.Converters.Add(SingleCaseUnionConverter())
jsonRpcFormatter.JsonSerializer.Converters.Add(OptionConverter())
jsonRpcFormatter.JsonSerializer.Converters.Add(ErasedUnionConverter())
jsonRpcFormatter.JsonSerializer.ContractResolver <- OptionAndCamelCasePropertyNamesContractResolver()
let defaultJsonRpcFormatter() =
let jsonRpcFormatter = new JsonMessageFormatter()
jsonRpcFormatter.JsonSerializer.NullValueHandling <- NullValueHandling.Ignore
jsonRpcFormatter.JsonSerializer.ConstructorHandling <- ConstructorHandling.AllowNonPublicDefaultConstructor
jsonRpcFormatter.JsonSerializer.MissingMemberHandling <- MissingMemberHandling.Ignore
jsonRpcFormatter.JsonSerializer.Converters.Add(StrictNumberConverter())
jsonRpcFormatter.JsonSerializer.Converters.Add(StrictStringConverter())
jsonRpcFormatter.JsonSerializer.Converters.Add(StrictBoolConverter())
jsonRpcFormatter.JsonSerializer.Converters.Add(SingleCaseUnionConverter())
jsonRpcFormatter.JsonSerializer.Converters.Add(OptionConverter())
jsonRpcFormatter.JsonSerializer.Converters.Add(ErasedUnionConverter())
jsonRpcFormatter.JsonSerializer.ContractResolver <- OptionAndCamelCasePropertyNamesContractResolver()
jsonRpcFormatter

let jsonRpcFormatter = defaultJsonRpcFormatter()

let deserialize<'t> (token: JToken) = token.ToObject<'t>(jsonRpcFormatter.JsonSerializer)
let serialize<'t> (o: 't) = JToken.FromObject(o, jsonRpcFormatter.JsonSerializer)
Expand Down Expand Up @@ -95,7 +99,7 @@ module Server =
(customizeRpc: IJsonRpcMessageHandler -> JsonRpc)
=

use jsonRpcHandler = new HeaderDelimitedMessageHandler(output, input, jsonRpcFormatter)
use jsonRpcHandler = new HeaderDelimitedMessageHandler(output, input, defaultJsonRpcFormatter())
// Without overriding isFatalException, JsonRpc serializes exceptions and sends them to the client.
// This is particularly bad for notifications such as textDocument/didChange which don't require a response,
// and thus any exception that happens during e.g. text sync gets swallowed.
Expand Down
1 change: 1 addition & 0 deletions tests/Ionide.LanguageServerProtocol.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">

Check failure on line 1 in tests/Ionide.LanguageServerProtocol.Tests.fsproj

View workflow job for this annotation

GitHub Actions / build (ubuntu-latest)

LSP.(de)serialization.startWithSetup.can start up multiple times in same process

Header does not end with expected character sequence: Content-Length: 44

Check failure on line 1 in tests/Ionide.LanguageServerProtocol.Tests.fsproj

View workflow job for this annotation

GitHub Actions / build (windows-latest)

LSP.(de)serialization.startWithSetup.can start up multiple times in same process

Header does not end with expected character sequence: Content-Length: 44

Check failure on line 1 in tests/Ionide.LanguageServerProtocol.Tests.fsproj

View workflow job for this annotation

GitHub Actions / build (macOS-latest)

LSP.(de)serialization.startWithSetup.can start up multiple times in same process

Header does not end with expected character sequence: Content-Length: 44

<PropertyGroup>
<OutputType>Exe</OutputType>
Expand All @@ -10,6 +10,7 @@
<Compile Include="Utils.fs" />
<Compile Include="Benchmarks.fs" />
<Compile Include="Shotgun.fs" />
<Compile Include="StartWithSetup.fs" />
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
Expand Down
74 changes: 74 additions & 0 deletions tests/StartWithSetup.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
module Ionide.LanguageServerProtocol.Tests.StartWithSetup

open Expecto
open System.IO.Pipes
open System.IO
open Ionide.LanguageServerProtocol
open Ionide.LanguageServerProtocol.Server

type TestLspClient(sendServerNotification: ClientNotificationSender, sendServerRequest: ClientRequestSender) =
inherit LspClient ()

let setupEndpoints(_: LspClient): Map<string, System.Delegate> =
[] |> Map.ofList

let requestWithContentLength(request: string) =
Copy link

Choose a reason for hiding this comment

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

let requestWithContentLength(request: string) = $"Content-Length: {request.Length}\r\n\r\n{request}"

git can convert newlines in files, and since this is a string literal, it caused a problem when the underlying file bytes changed. It was coded on windows which is \r\n, but got converted to unix line endings on push which is \n only. The json rpc stream requires \r\n.

Their error message is actually bugged. They don't escape the slashes in their string so it prints an actual newline where they intended to print "\r\n". I'll probably make a PR over there.

Also note it can't be a @ literal string now, or that would literally put \r\n in the string instead of escaping and putting the return and new line bytes.

@$"Content-Length: {request.Length}

{request}"

let shutdownRequest = @"{""jsonrpc"":""2.0"",""method"":""shutdown"",""id"":1}"

let exitRequest = @"{""jsonrpc"":""2.0"",""method"":""exit"",""id"":1}"

let tests =
testList
"startWithSetup"
[
testAsync "can start up multiple times in same process" {
use inputServerPipe1 = new AnonymousPipeServerStream()
use inputClientPipe1 = new AnonymousPipeClientStream(inputServerPipe1.GetClientHandleAsString())
use outputServerPipe1 = new AnonymousPipeServerStream()

use inputWriter1 = new StreamWriter(inputServerPipe1)
inputWriter1.AutoFlush <- true
let server1 = async {
let result = (startWithSetup
setupEndpoints
inputClientPipe1
outputServerPipe1
TestLspClient
defaultRpc)
Expect.equal (int result) 0 "server startup failed"
}

let! server1Async = Async.StartChild(server1)

use inputServerPipe2 = new AnonymousPipeServerStream()
use inputClientPipe2 = new AnonymousPipeClientStream(inputServerPipe2.GetClientHandleAsString())
use outputServerPipe2 = new AnonymousPipeServerStream()

use inputWriter2 = new StreamWriter(inputServerPipe2)
inputWriter2.AutoFlush <- true
let server2 = async {
let result = (startWithSetup
setupEndpoints
inputClientPipe2
outputServerPipe2
TestLspClient
defaultRpc)
Expect.equal (int result) 0 "server startup failed"
}

let! server2Async = Async.StartChild(server2)

inputWriter1.Write(requestWithContentLength(shutdownRequest))
inputWriter1.Write(requestWithContentLength(exitRequest))

inputWriter2.Write(requestWithContentLength(shutdownRequest))
inputWriter2.Write(requestWithContentLength(exitRequest))

do! server1Async
do! server2Async
}
]
3 changes: 2 additions & 1 deletion tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -874,7 +874,8 @@ let private serializationTests =
Data = None }
testThereAndBackAgain item
]
Shotgun.tests ]
Shotgun.tests
StartWithSetup.tests ]

[<Tests>]
let tests = testList "LSP" [ serializationTests; Utils.tests ]
Copy link

Choose a reason for hiding this comment

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

I just notice with Utils.Tests being here, maybe StartWithSetup.tests should be too, rather than at the end of the serializationTests. Dunno what maintainers want.
let tests = testList "LSP" [ serializationTests; Utils.tests; StartWithSetup.tests ]