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

Support pipe & socket flags for language server startup #5852

Merged
merged 4 commits into from
Feb 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/Bicep.Cli.IntegrationTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@
"System.Xml.XmlDocument": "4.3.0"
}
},
"CommandLineParser": {
"type": "Transitive",
"resolved": "2.8.0",
"contentHash": "eco2HlKQBY4Joz9odHigzGpVzv6pjsXnY5lziioMveQxr+i2Z7xYcIOMeZTgYiqnMtMAbXMXsVhrNfWO5vJS8Q=="
},
"DiffPlex": {
"type": "Transitive",
"resolved": "1.7.0",
Expand Down Expand Up @@ -1775,6 +1780,7 @@
"type": "Project",
"dependencies": {
"Azure.Bicep.Core": "1.0.0",
"CommandLineParser": "2.8.0",
"OmniSharp.Extensions.LanguageServer": "0.19.2"
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Bicep.Core.IntegrationTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@
"System.Xml.XmlDocument": "4.3.0"
}
},
"CommandLineParser": {
"type": "Transitive",
"resolved": "2.8.0",
"contentHash": "eco2HlKQBY4Joz9odHigzGpVzv6pjsXnY5lziioMveQxr+i2Z7xYcIOMeZTgYiqnMtMAbXMXsVhrNfWO5vJS8Q=="
},
"DiffPlex": {
"type": "Transitive",
"resolved": "1.7.0",
Expand Down Expand Up @@ -1765,6 +1770,7 @@
"type": "Project",
"dependencies": {
"Azure.Bicep.Core": "1.0.0",
"CommandLineParser": "2.8.0",
"OmniSharp.Extensions.LanguageServer": "0.19.2"
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Bicep.Core.Samples/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@
"System.Xml.XmlDocument": "4.3.0"
}
},
"CommandLineParser": {
"type": "Transitive",
"resolved": "2.8.0",
"contentHash": "eco2HlKQBY4Joz9odHigzGpVzv6pjsXnY5lziioMveQxr+i2Z7xYcIOMeZTgYiqnMtMAbXMXsVhrNfWO5vJS8Q=="
},
"DiffPlex": {
"type": "Transitive",
"resolved": "1.7.0",
Expand Down Expand Up @@ -1753,6 +1758,7 @@
"type": "Project",
"dependencies": {
"Azure.Bicep.Core": "1.0.0",
"CommandLineParser": "2.8.0",
"OmniSharp.Extensions.LanguageServer": "0.19.2"
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Bicep.Core.UnitTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@
"System.Xml.XmlDocument": "4.3.0"
}
},
"CommandLineParser": {
"type": "Transitive",
"resolved": "2.8.0",
"contentHash": "eco2HlKQBY4Joz9odHigzGpVzv6pjsXnY5lziioMveQxr+i2Z7xYcIOMeZTgYiqnMtMAbXMXsVhrNfWO5vJS8Q=="
},
"Json.More.Net": {
"type": "Transitive",
"resolved": "1.4.4",
Expand Down Expand Up @@ -1719,6 +1724,7 @@
"type": "Project",
"dependencies": {
"Azure.Bicep.Core": "1.0.0",
"CommandLineParser": "2.8.0",
"OmniSharp.Extensions.LanguageServer": "0.19.2"
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Bicep.Decompiler.IntegrationTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@
"System.Xml.XmlDocument": "4.3.0"
}
},
"CommandLineParser": {
"type": "Transitive",
"resolved": "2.8.0",
"contentHash": "eco2HlKQBY4Joz9odHigzGpVzv6pjsXnY5lziioMveQxr+i2Z7xYcIOMeZTgYiqnMtMAbXMXsVhrNfWO5vJS8Q=="
},
"DiffPlex": {
"type": "Transitive",
"resolved": "1.7.0",
Expand Down Expand Up @@ -1738,6 +1743,7 @@
"type": "Project",
"dependencies": {
"Azure.Bicep.Core": "1.0.0",
"CommandLineParser": "2.8.0",
"OmniSharp.Extensions.LanguageServer": "0.19.2"
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Bicep.Decompiler.UnitTests/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,11 @@
"System.Xml.XmlDocument": "4.3.0"
}
},
"CommandLineParser": {
"type": "Transitive",
"resolved": "2.8.0",
"contentHash": "eco2HlKQBY4Joz9odHigzGpVzv6pjsXnY5lziioMveQxr+i2Z7xYcIOMeZTgYiqnMtMAbXMXsVhrNfWO5vJS8Q=="
},
"DiffPlex": {
"type": "Transitive",
"resolved": "1.7.0",
Expand Down Expand Up @@ -1738,6 +1743,7 @@
"type": "Project",
"dependencies": {
"Azure.Bicep.Core": "1.0.0",
"CommandLineParser": "2.8.0",
"OmniSharp.Extensions.LanguageServer": "0.19.2"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ public static async Task<LanguageServerHelper> StartServerWithClientConnectionAs
ModuleRestoreScheduler = creationOptions.ModuleRestoreScheduler ?? BicepTestConstants.ModuleRestoreScheduler
};

var server = new Server(serverPipe.Reader, clientPipe.Writer, creationOptions);
var server = new Server(
creationOptions,
options => options
.WithInput(serverPipe.Reader)
.WithOutput(clientPipe.Writer));
var _ = server.RunAsync(CancellationToken.None); // do not wait on this async method, or you'll be waiting a long time!

var client = LanguageClient.PreInit(options =>
Expand Down
256 changes: 256 additions & 0 deletions src/Bicep.LangServer.IntegrationTests/InputOutputTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Bicep.Core.Extensions;
using Bicep.Core.FileSystem;
using Bicep.Core.Navigation;
using Bicep.Core.Parsing;
using Bicep.Core.Samples;
using Bicep.Core.Semantics;
using Bicep.Core.Syntax;
using Bicep.Core.Syntax.Visitors;
using Bicep.Core.Text;
using Bicep.Core.TypeSystem.Az;
using Bicep.Core.UnitTests;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Utils;
using Bicep.Core.Workspaces;
using Bicep.LangServer.IntegrationTests.Assertions;
using Bicep.LangServer.IntegrationTests.Extensions;
using Bicep.LangServer.IntegrationTests.Helpers;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OmniSharp.Extensions.LanguageServer.Client;
using OmniSharp.Extensions.LanguageServer.Protocol;
using OmniSharp.Extensions.LanguageServer.Protocol.Client;
using OmniSharp.Extensions.LanguageServer.Protocol.Document;
using OmniSharp.Extensions.LanguageServer.Protocol.Models;
using OmniSharp.Extensions.LanguageServer.Protocol.Window;
using SymbolKind = Bicep.Core.Semantics.SymbolKind;

namespace Bicep.LangServer.IntegrationTests
{
[TestClass]
public class InputOutputTests
{
[NotNull]
public TestContext? TestContext { get; set; }

private CancellationToken GetCancellationTokenWithTimeout(TimeSpan timeSpan)
=> CancellationTokenSource.CreateLinkedTokenSource(
new CancellationTokenSource(timeSpan).Token,
TestContext.CancellationTokenSource.Token).Token;

private static Process StartServerProcessWithConsoleIO()
Copy link
Member

Choose a reason for hiding this comment

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

StartServerProcessWithConsoleIO

All 3 versions of this differ only in the arguments. Worth refactoring?

{
var exePath = typeof(LanguageServer.Program).Assembly.Location;

var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = exePath,
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true,
},
};

process.Start();

return process;
}

private static Process StartServerProcessWithNamedPipeIo(string pipeName)
{
var exePath = typeof(LanguageServer.Program).Assembly.Location;

var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"{exePath} --pipe {pipeName}",
UseShellExecute = false,
RedirectStandardError = true,
Copy link
Member

Choose a reason for hiding this comment

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

RedirectStandardError

If you don't have something to consume the stderr and stdout, you can get a deadlock. (Easiest is just to add dummy event handlers with the async redirection.)

RedirectStandardOutput = true,
RedirectStandardInput = true,
},
};

process.Start();

return process;
}

private static Process StartServerProcessWithSocketIo(int port)
{
var exePath = typeof(LanguageServer.Program).Assembly.Location;

var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"{exePath} --socket {port}",
UseShellExecute = false,
RedirectStandardError = true,
RedirectStandardOutput = true,
RedirectStandardInput = true,
},
};

process.Start();

return process;
}

private async Task<ILanguageClient> InitializeLanguageClient(Stream inputStream, Stream outputStream, MultipleMessageListener<PublishDiagnosticsParams> publishDiagnosticsListener, CancellationToken cancellationToken)
{
var client = LanguageClient.PreInit(options =>
{
options
.WithInput(inputStream)
.WithOutput(outputStream)
.OnInitialize((client, request, cancellationToken) => { TestContext.WriteLine("Language client initializing."); return Task.CompletedTask; })
.OnInitialized((client, request, response, cancellationToken) => { TestContext.WriteLine("Language client initialized."); return Task.CompletedTask; })
.OnStarted((client, cancellationToken) => { TestContext.WriteLine("Language client started."); return Task.CompletedTask; })
.OnLogTrace(@params => TestContext.WriteLine($"TRACE: {@params.Message} VERBOSE: {@params.Verbose}"))
.OnLogMessage(@params => TestContext.WriteLine($"{@params.Type}: {@params.Message}"))
.OnPublishDiagnostics(x => publishDiagnosticsListener.AddMessage(x));
});

await client.Initialize(cancellationToken);

return client;
}

[TestMethod]
public async Task ServerProcess_e2e_test_with_console_io()
{
var cancellationToken = GetCancellationTokenWithTimeout(TimeSpan.FromSeconds(60));
var publishDiagsListener = new MultipleMessageListener<PublishDiagnosticsParams>();
var documentUri = DocumentUri.From("/template.bicep");
var bicepFile = @"
#disable-next-line no-unused-params
param foo string = 123 // trigger a type error
";

using var process = StartServerProcessWithConsoleIO();
try
{
var input = process.StandardOutput.BaseStream;
var output = process.StandardInput.BaseStream;

using var client = await InitializeLanguageClient(input, output, publishDiagsListener, cancellationToken);

client.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(documentUri, bicepFile, 0));
var publishDiagsResult = await publishDiagsListener.WaitNext();

publishDiagsResult.Diagnostics.Should().SatisfyRespectively(
d =>
{
d.Range.Should().HaveRange((2, 19), (2, 22));
d.Should().HaveCodeAndSeverity("BCP027", DiagnosticSeverity.Error);
});
}
finally
{
process.Kill(entireProcessTree: true);
process.Dispose();
}
}

[TestMethod]
public async Task ServerProcess_e2e_test_with_named_pipes_io()
{
var cancellationToken = GetCancellationTokenWithTimeout(TimeSpan.FromSeconds(60));
var publishDiagsListener = new MultipleMessageListener<PublishDiagnosticsParams>();
var documentUri = DocumentUri.From("/template.bicep");
var bicepFile = @"
#disable-next-line no-unused-params
param foo string = 123 // trigger a type error
";

var pipeName = Guid.NewGuid().ToString();
using var pipeStream = new NamedPipeServerStream(pipeName, PipeDirection.InOut, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
using var process = StartServerProcessWithNamedPipeIo(pipeName);
try
{
await pipeStream.WaitForConnectionAsync(cancellationToken);

using var client = await InitializeLanguageClient(pipeStream, pipeStream, publishDiagsListener, cancellationToken);

client.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(documentUri, bicepFile, 0));
var publishDiagsResult = await publishDiagsListener.WaitNext();

publishDiagsResult.Diagnostics.Should().SatisfyRespectively(
d =>
{
d.Range.Should().HaveRange((2, 19), (2, 22));
d.Should().HaveCodeAndSeverity("BCP027", DiagnosticSeverity.Error);
});
}
finally
{
process.Kill(entireProcessTree: true);
process.Dispose();
}
}

[TestMethod]
public async Task ServerProcess_e2e_test_with_socket_io()
{
var cancellationToken = GetCancellationTokenWithTimeout(TimeSpan.FromSeconds(60));
var publishDiagsListener = new MultipleMessageListener<PublishDiagnosticsParams>();
var documentUri = DocumentUri.From("/template.bicep");
var bicepFile = @"
#disable-next-line no-unused-params
param foo string = 123 // trigger a type error
";

var tcpListener = new TcpListener(IPAddress.Loopback, 0);
tcpListener.Start();
var tcpPort = (tcpListener.LocalEndpoint as IPEndPoint)!.Port;

using var process = StartServerProcessWithSocketIo(tcpPort);

using var tcpClient = await tcpListener.AcceptTcpClientAsync(cancellationToken);
var tcpStream = tcpClient.GetStream();

try
{
using var client = await InitializeLanguageClient(tcpStream, tcpStream, publishDiagsListener, cancellationToken);

client.DidOpenTextDocument(TextDocumentParamHelper.CreateDidOpenDocumentParams(documentUri, bicepFile, 0));
var publishDiagsResult = await publishDiagsListener.WaitNext();

publishDiagsResult.Diagnostics.Should().SatisfyRespectively(
d =>
{
d.Range.Should().HaveRange((2, 19), (2, 22));
d.Should().HaveCodeAndSeverity("BCP027", DiagnosticSeverity.Error);
});
}
finally
{
process.Kill(entireProcessTree: true);
process.Dispose();
}
}
}
}
Loading