From 6f7f575d308fc58ea65677ca159e40b58ba669ae Mon Sep 17 00:00:00 2001 From: Anthony Martin Date: Wed, 2 Feb 2022 05:40:57 +0000 Subject: [PATCH 1/3] Support pipe & socket flags for language server startup --- .../packages.lock.json | 6 ++ .../packages.lock.json | 6 ++ src/Bicep.Core.Samples/packages.lock.json | 6 ++ src/Bicep.Core.UnitTests/packages.lock.json | 6 ++ .../packages.lock.json | 6 ++ .../packages.lock.json | 6 ++ .../Helpers/LanguageServerHelper.cs | 6 +- .../packages.lock.json | 6 ++ .../packages.lock.json | 6 ++ src/Bicep.LangServer/Bicep.LangServer.csproj | 1 + src/Bicep.LangServer/Program.cs | 97 +++++++++++++++++-- src/Bicep.LangServer/Server.cs | 12 +-- src/Bicep.LangServer/packages.lock.json | 6 ++ src/vscode-bicep/src/language/client.ts | 6 +- 14 files changed, 154 insertions(+), 22 deletions(-) diff --git a/src/Bicep.Cli.IntegrationTests/packages.lock.json b/src/Bicep.Cli.IntegrationTests/packages.lock.json index d2f82aa7a72..cbf9b938b61 100644 --- a/src/Bicep.Cli.IntegrationTests/packages.lock.json +++ b/src/Bicep.Cli.IntegrationTests/packages.lock.json @@ -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", @@ -1775,6 +1780,7 @@ "type": "Project", "dependencies": { "Azure.Bicep.Core": "1.0.0", + "CommandLineParser": "2.8.0", "OmniSharp.Extensions.LanguageServer": "0.19.2" } } diff --git a/src/Bicep.Core.IntegrationTests/packages.lock.json b/src/Bicep.Core.IntegrationTests/packages.lock.json index 37744556712..9b8946a3595 100644 --- a/src/Bicep.Core.IntegrationTests/packages.lock.json +++ b/src/Bicep.Core.IntegrationTests/packages.lock.json @@ -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", @@ -1765,6 +1770,7 @@ "type": "Project", "dependencies": { "Azure.Bicep.Core": "1.0.0", + "CommandLineParser": "2.8.0", "OmniSharp.Extensions.LanguageServer": "0.19.2" } } diff --git a/src/Bicep.Core.Samples/packages.lock.json b/src/Bicep.Core.Samples/packages.lock.json index 1432528425d..0829ed7fa33 100644 --- a/src/Bicep.Core.Samples/packages.lock.json +++ b/src/Bicep.Core.Samples/packages.lock.json @@ -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", @@ -1753,6 +1758,7 @@ "type": "Project", "dependencies": { "Azure.Bicep.Core": "1.0.0", + "CommandLineParser": "2.8.0", "OmniSharp.Extensions.LanguageServer": "0.19.2" } } diff --git a/src/Bicep.Core.UnitTests/packages.lock.json b/src/Bicep.Core.UnitTests/packages.lock.json index d521382dc5b..6223ba10f83 100644 --- a/src/Bicep.Core.UnitTests/packages.lock.json +++ b/src/Bicep.Core.UnitTests/packages.lock.json @@ -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", @@ -1719,6 +1724,7 @@ "type": "Project", "dependencies": { "Azure.Bicep.Core": "1.0.0", + "CommandLineParser": "2.8.0", "OmniSharp.Extensions.LanguageServer": "0.19.2" } } diff --git a/src/Bicep.Decompiler.IntegrationTests/packages.lock.json b/src/Bicep.Decompiler.IntegrationTests/packages.lock.json index fbc6148fdf4..fb712251539 100644 --- a/src/Bicep.Decompiler.IntegrationTests/packages.lock.json +++ b/src/Bicep.Decompiler.IntegrationTests/packages.lock.json @@ -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", @@ -1738,6 +1743,7 @@ "type": "Project", "dependencies": { "Azure.Bicep.Core": "1.0.0", + "CommandLineParser": "2.8.0", "OmniSharp.Extensions.LanguageServer": "0.19.2" } } diff --git a/src/Bicep.Decompiler.UnitTests/packages.lock.json b/src/Bicep.Decompiler.UnitTests/packages.lock.json index fbc6148fdf4..fb712251539 100644 --- a/src/Bicep.Decompiler.UnitTests/packages.lock.json +++ b/src/Bicep.Decompiler.UnitTests/packages.lock.json @@ -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", @@ -1738,6 +1743,7 @@ "type": "Project", "dependencies": { "Azure.Bicep.Core": "1.0.0", + "CommandLineParser": "2.8.0", "OmniSharp.Extensions.LanguageServer": "0.19.2" } } diff --git a/src/Bicep.LangServer.IntegrationTests/Helpers/LanguageServerHelper.cs b/src/Bicep.LangServer.IntegrationTests/Helpers/LanguageServerHelper.cs index ec1e6133d01..75c5456b1cf 100644 --- a/src/Bicep.LangServer.IntegrationTests/Helpers/LanguageServerHelper.cs +++ b/src/Bicep.LangServer.IntegrationTests/Helpers/LanguageServerHelper.cs @@ -48,7 +48,11 @@ public static async Task 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 => diff --git a/src/Bicep.LangServer.IntegrationTests/packages.lock.json b/src/Bicep.LangServer.IntegrationTests/packages.lock.json index 363dd8d8693..3983b3ad9ae 100644 --- a/src/Bicep.LangServer.IntegrationTests/packages.lock.json +++ b/src/Bicep.LangServer.IntegrationTests/packages.lock.json @@ -216,6 +216,11 @@ "System.Xml.XmlDocument": "4.3.0" } }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.8.0", + "contentHash": "eco2HlKQBY4Joz9odHigzGpVzv6pjsXnY5lziioMveQxr+i2Z7xYcIOMeZTgYiqnMtMAbXMXsVhrNfWO5vJS8Q==" + }, "DiffPlex": { "type": "Transitive", "resolved": "1.7.0", @@ -1778,6 +1783,7 @@ "type": "Project", "dependencies": { "Azure.Bicep.Core": "1.0.0", + "CommandLineParser": "2.8.0", "OmniSharp.Extensions.LanguageServer": "0.19.2" } } diff --git a/src/Bicep.LangServer.UnitTests/packages.lock.json b/src/Bicep.LangServer.UnitTests/packages.lock.json index 3fee3dfeb50..2c2e0eed52d 100644 --- a/src/Bicep.LangServer.UnitTests/packages.lock.json +++ b/src/Bicep.LangServer.UnitTests/packages.lock.json @@ -216,6 +216,11 @@ "System.Xml.XmlDocument": "4.3.0" } }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.8.0", + "contentHash": "eco2HlKQBY4Joz9odHigzGpVzv6pjsXnY5lziioMveQxr+i2Z7xYcIOMeZTgYiqnMtMAbXMXsVhrNfWO5vJS8Q==" + }, "DiffPlex": { "type": "Transitive", "resolved": "1.7.0", @@ -1778,6 +1783,7 @@ "type": "Project", "dependencies": { "Azure.Bicep.Core": "1.0.0", + "CommandLineParser": "2.8.0", "OmniSharp.Extensions.LanguageServer": "0.19.2" } }, diff --git a/src/Bicep.LangServer/Bicep.LangServer.csproj b/src/Bicep.LangServer/Bicep.LangServer.csproj index a64980e58cf..0b352b84666 100644 --- a/src/Bicep.LangServer/Bicep.LangServer.csproj +++ b/src/Bicep.LangServer/Bicep.LangServer.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Bicep.LangServer/Program.cs b/src/Bicep.LangServer/Program.cs index e9d5b79074f..0794b2a2047 100644 --- a/src/Bicep.LangServer/Program.cs +++ b/src/Bicep.LangServer/Program.cs @@ -1,32 +1,111 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; +using System.Linq; +using System.IO.Pipes; +using System.Net.Sockets; using System.Runtime; using System.Threading; using System.Threading.Tasks; +using CommandLine; using Bicep.Core.Utils; +using System.IO; +using System.Net; +using System.Diagnostics; namespace Bicep.LanguageServer { public class Program { - public static async Task Main() + public class CommandLineOptions + { + [Option("pipe", Required = false, HelpText = "The named pipe to connect to for LSP communication")] + public string? Pipe { get; set; } + + [Option("socket", Required = false, HelpText = "The TCP socket to connect to for LSP communication")] + public string? Socket { get; set; } + + [Option("stdio", Required = false, HelpText = "If set, use stdin/stdout for LSP communication")] + public bool Stdio { get; set; } + + [Option("wait-for-debugger", Required = false, HelpText = "If set, wait for a dotnet debugger to be attached before starting the server")] + public bool WaitForDebugger { get; set; } + } + + public static async Task Main(string[] args) => await RunWithCancellationAsync(async cancellationToken => { - string profilePath = DirHelper.GetTempPath(); + var profilePath = DirHelper.GetTempPath(); ProfileOptimization.SetProfileRoot(profilePath); ProfileOptimization.StartProfile("bicepserver.profile"); - // the server uses JSON-RPC over stdin & stdout to communicate, - // so be careful not to use console for logging! - var server = new Server( - Console.OpenStandardInput(), - Console.OpenStandardOutput(), - new Server.CreationOptions()); + var parser = new Parser(settings => { + settings.IgnoreUnknownArguments = true; + }); - await server.RunAsync(cancellationToken); + await parser.ParseArguments(args) + .WithNotParsed((x) => Environment.Exit(1)) + .WithParsedAsync(async options => await RunServer(options, cancellationToken)); }); + private static async Task RunServer(CommandLineOptions options, CancellationToken cancellationToken) + { + if (options.WaitForDebugger) + { + while (!Debugger.IsAttached) + { + await Task.Delay(100, cancellationToken); + } + + Debugger.Break(); + } + + Server server; + if (options.Pipe is not null) + { + var pipeName = options.Pipe; + if (pipeName.StartsWith(@"\\.\pipe\")) + { + pipeName = pipeName.Substring(@"\\.\pipe\".Length); + } + + var clientPipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); + + await clientPipe.ConnectAsync(cancellationToken); + + server = new( + new(), + options => options + .WithInput(clientPipe) + .WithOutput(clientPipe)); + } + else if (options.Socket is not null) + { + var port = short.Parse(options.Socket); + var tcpClient = new TcpClient(); + + await tcpClient.ConnectAsync(IPAddress.Loopback, port, cancellationToken); + var tcpStream = tcpClient.GetStream(); + + server = new( + new(), + options => options + .WithInput(tcpStream) + .WithOutput(tcpStream) + .RegisterForDisposal(tcpClient)); + } + else + { + server = new( + new(), + options => options + .WithInput(Console.OpenStandardInput()) + .WithOutput(Console.OpenStandardOutput())); + } + + await server.RunAsync(cancellationToken); + } + private static async Task RunWithCancellationAsync(Func runFunc) { var cancellationTokenSource = new CancellationTokenSource(); diff --git a/src/Bicep.LangServer/Server.cs b/src/Bicep.LangServer/Server.cs index f2d37688bc8..dfd98cf552f 100644 --- a/src/Bicep.LangServer/Server.cs +++ b/src/Bicep.LangServer/Server.cs @@ -48,17 +48,7 @@ public record CreationOptions( private readonly OmnisharpLanguageServer server; - public Server(PipeReader input, PipeWriter output, CreationOptions creationOptions) - : this(creationOptions, options => options.WithInput(input).WithOutput(output)) - { - } - - public Server(Stream input, Stream output, CreationOptions creationOptions) - : this(creationOptions, options => options.WithInput(input).WithOutput(output)) - { - } - - private Server(CreationOptions creationOptions, Action onOptionsFunc) + public Server(CreationOptions creationOptions, Action onOptionsFunc) { BicepDeploymentsInterop.Initialize(); server = OmnisharpLanguageServer.PreInit(options => diff --git a/src/Bicep.LangServer/packages.lock.json b/src/Bicep.LangServer/packages.lock.json index 2251c3f5138..16d77b1d03e 100644 --- a/src/Bicep.LangServer/packages.lock.json +++ b/src/Bicep.LangServer/packages.lock.json @@ -2,6 +2,12 @@ "version": 1, "dependencies": { "net6.0": { + "CommandLineParser": { + "type": "Direct", + "requested": "[2.8.0, )", + "resolved": "2.8.0", + "contentHash": "eco2HlKQBY4Joz9odHigzGpVzv6pjsXnY5lziioMveQxr+i2Z7xYcIOMeZTgYiqnMtMAbXMXsVhrNfWO5vJS8Q==" + }, "Microsoft.CodeAnalysis.BannedApiAnalyzers": { "type": "Direct", "requested": "[3.3.3, )", diff --git a/src/vscode-bicep/src/language/client.ts b/src/vscode-bicep/src/language/client.ts index 04c59c1dc96..fc8c5148ffa 100644 --- a/src/vscode-bicep/src/language/client.ts +++ b/src/vscode-bicep/src/language/client.ts @@ -43,7 +43,11 @@ async function launchLanguageService( const serverExecutable: lsp.Executable = { command: dotnetCommandPath, - args: [languageServerPath], + args: [ + languageServerPath, + // uncomment the next line to pause server startup until a dotnet debugger has been attached + // '--wait-for-debugger' + ], options: { env: process.env, }, From 447c4428a1d7f9b4fb5b031381a2bf4ff1d94905 Mon Sep 17 00:00:00 2001 From: Anthony Martin Date: Wed, 2 Feb 2022 20:32:55 -0500 Subject: [PATCH 2/3] Address PR comments, add tests --- .../InputOutputTests.cs | 256 ++++++++++++++++++ src/Bicep.LangServer/Program.cs | 18 +- 2 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 src/Bicep.LangServer.IntegrationTests/InputOutputTests.cs diff --git a/src/Bicep.LangServer.IntegrationTests/InputOutputTests.cs b/src/Bicep.LangServer.IntegrationTests/InputOutputTests.cs new file mode 100644 index 00000000000..9e4a2c0d857 --- /dev/null +++ b/src/Bicep.LangServer.IntegrationTests/InputOutputTests.cs @@ -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() + { + 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, + 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 InitializeLanguageClient(Stream inputStream, Stream outputStream, MultipleMessageListener 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(); + 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(); + var documentUri = DocumentUri.From("/template.bicep"); + var bicepFile = @" +#disable-next-line no-unused-params +param foo string = 123 // trigger a type error +"; + + var pipeName = $"mrPipey-{Guid.NewGuid()}"; + 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(); + 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.Any, 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(); + } + } + } +} diff --git a/src/Bicep.LangServer/Program.cs b/src/Bicep.LangServer/Program.cs index 0794b2a2047..32ef27e8f10 100644 --- a/src/Bicep.LangServer/Program.cs +++ b/src/Bicep.LangServer/Program.cs @@ -22,8 +22,8 @@ public class CommandLineOptions [Option("pipe", Required = false, HelpText = "The named pipe to connect to for LSP communication")] public string? Pipe { get; set; } - [Option("socket", Required = false, HelpText = "The TCP socket to connect to for LSP communication")] - public string? Socket { get; set; } + [Option("socket", Required = false, HelpText = "The TCP port to connect to for LSP communication")] + public short? Socket { get; set; } [Option("stdio", Required = false, HelpText = "If set, use stdin/stdout for LSP communication")] public bool Stdio { get; set; } @@ -52,20 +52,25 @@ private static async Task RunServer(CommandLineOptions options, CancellationToke { if (options.WaitForDebugger) { + // exit if we don't have a debugger attached within 5 minutes + var debuggerTimeoutToken = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, + new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token).Token; + while (!Debugger.IsAttached) { - await Task.Delay(100, cancellationToken); + await Task.Delay(100, debuggerTimeoutToken); } Debugger.Break(); } Server server; - if (options.Pipe is not null) + if (options.Pipe is { } pipeName) { - var pipeName = options.Pipe; if (pipeName.StartsWith(@"\\.\pipe\")) { + // VSCode on Windows prefixes the pipe with \\.\pipe\ pipeName = pipeName.Substring(@"\\.\pipe\".Length); } @@ -79,9 +84,8 @@ private static async Task RunServer(CommandLineOptions options, CancellationToke .WithInput(clientPipe) .WithOutput(clientPipe)); } - else if (options.Socket is not null) + else if (options.Socket is { } port) { - var port = short.Parse(options.Socket); var tcpClient = new TcpClient(); await tcpClient.ConnectAsync(IPAddress.Loopback, port, cancellationToken); From 96503d74d9992e91d33f06ad33b3aa17ef69e6c3 Mon Sep 17 00:00:00 2001 From: Anthony Martin Date: Thu, 3 Feb 2022 02:47:07 +0000 Subject: [PATCH 3/3] Fix socket test --- src/Bicep.LangServer.IntegrationTests/InputOutputTests.cs | 4 ++-- src/Bicep.LangServer/Program.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bicep.LangServer.IntegrationTests/InputOutputTests.cs b/src/Bicep.LangServer.IntegrationTests/InputOutputTests.cs index 9e4a2c0d857..66e094bec9a 100644 --- a/src/Bicep.LangServer.IntegrationTests/InputOutputTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/InputOutputTests.cs @@ -186,7 +186,7 @@ public async Task ServerProcess_e2e_test_with_named_pipes_io() param foo string = 123 // trigger a type error "; - var pipeName = $"mrPipey-{Guid.NewGuid()}"; + 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 @@ -223,7 +223,7 @@ public async Task ServerProcess_e2e_test_with_socket_io() param foo string = 123 // trigger a type error "; - var tcpListener = new TcpListener(IPAddress.Any, 0); + var tcpListener = new TcpListener(IPAddress.Loopback, 0); tcpListener.Start(); var tcpPort = (tcpListener.LocalEndpoint as IPEndPoint)!.Port; diff --git a/src/Bicep.LangServer/Program.cs b/src/Bicep.LangServer/Program.cs index 32ef27e8f10..a65fcc22328 100644 --- a/src/Bicep.LangServer/Program.cs +++ b/src/Bicep.LangServer/Program.cs @@ -23,7 +23,7 @@ public class CommandLineOptions public string? Pipe { get; set; } [Option("socket", Required = false, HelpText = "The TCP port to connect to for LSP communication")] - public short? Socket { get; set; } + public int? Socket { get; set; } [Option("stdio", Required = false, HelpText = "If set, use stdin/stdout for LSP communication")] public bool Stdio { get; set; }