csharp-language-server-protocol icon indicating copy to clipboard operation
csharp-language-server-protocol copied to clipboard

Feature Request: Support handling LSP command line options for input/output

Open davidmatson opened this issue 4 years ago • 1 comments

The language server protocol documents how to configure input/output via command line options.

It would be great if an extension method were available that handled command line options in this format and configured input/output accordingly.

Here's an example of what it might look like:

using OmniSharp.Extensions.LanguageServer.Server;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;

static class LanguageServerOptionsCommandLineExtensions
{
    public static LanguageServerOptions WithCommandLineCommunicationChannel(this LanguageServerOptions options,
        string[] args)
    {
        if (options == null)
        {
            throw new ArgumentNullException(nameof(options));
        }

        CommandLineOptions commandLineOptions = CommandLineOptions.Parse(args);

        switch (commandLineOptions.CommunicationChannel)
        {
            case CommunicationChannel.ConsoleInputOutput:
                options.WithInput(Console.OpenStandardInput());
                options.WithOutput(Console.OpenStandardOutput());
                break;
            case CommunicationChannel.Pipe:
                NamedPipeClientStream pipe = new NamedPipeClientStream(".", commandLineOptions.PipeName,
                    PipeDirection.InOut, PipeOptions.Asynchronous);
                // TODO: Make async? How to do that inside LanguageServer.From(Action<LanguageServerOptions>)?
                //await pipe.ConnectAsync(cancellationToken);
                pipe.Connect();
                options.WithInput(pipe);
                options.WithOutput(pipe);
                break;
            default:
                Debug.Assert(commandLineOptions.CommunicationChannel == CommunicationChannel.Socket);
                TcpClient client = new TcpClient();
                options.RegisterForDisposal(client);
                // TODO: Make async? How to do that inside LanguageServer.From(Action<LanguageServerOptions>)?
                //await client.ConnectAsync(IPAddress.Loopback, commandLineOptions.Port, cancellationToken);
                client.Connect(IPAddress.Loopback, commandLineOptions.Port);
                NetworkStream stream = client.GetStream();
                options.WithInput(stream);
                options.WithOutput(stream);
                break;
        }

        return options;
    }

    enum CommunicationChannel
    {
        ConsoleInputOutput,
        Pipe,
        Socket
    }

    struct CommandLineOptions
    {
        public CommunicationChannel CommunicationChannel { get; init; }

        public string PipeName { get; init; }

        public int Port { get; init; }

        public static CommandLineOptions Parse(string[] args)
        {
            if (args == null || args.Length == 0)
            {
                return new CommandLineOptions { CommunicationChannel = CommunicationChannel.ConsoleInputOutput };
            }

            if (args.Length <= 2)
            {
                string firstArgument = args[0];
                string secondArgument = args.Length > 1 ? args[1] : null;

                if (firstArgument == "--stdio" && secondArgument == null)
                {
                    return new CommandLineOptions { CommunicationChannel = CommunicationChannel.ConsoleInputOutput };
                }
                else if (firstArgument.StartsWith("--pipe"))
                {
                    // TODO: Handle --pipe appropriately when not running on Windows.
                    if (firstArgument == "--pipe" && secondArgument != null && secondArgument.StartsWith(@"\\.\pipe\"))
                    {
                        return new CommandLineOptions
                        {
                            CommunicationChannel = CommunicationChannel.Pipe,
                            PipeName = secondArgument.Substring(@"\\.\pipe\".Length)
                        };
                    }
                    else if (firstArgument.StartsWith(@"--pipe=\\.\pipe\") && secondArgument == null)
                    {
                        string pipeName = firstArgument.Substring(@"--pipe=\\.\pipe\".Length);

                        return new CommandLineOptions
                        {
                            CommunicationChannel = CommunicationChannel.Pipe,
                            PipeName = pipeName
                        };
                    }
                }
                else if (firstArgument.StartsWith("--socket"))
                {
                    if (firstArgument == "--socket" && secondArgument != null)
                    {
                        return new CommandLineOptions
                        {
                            CommunicationChannel = CommunicationChannel.Socket,
                            Port = int.Parse(secondArgument, NumberStyles.None, CultureInfo.InvariantCulture)
                        };
                    }
                    else if (firstArgument.StartsWith("--socket=") && secondArgument == null)
                    {
                        int port = int.Parse(firstArgument.Substring("--socket=".Length), NumberStyles.None,
                            CultureInfo.InvariantCulture);
                        return new CommandLineOptions
                        {
                            CommunicationChannel = CommunicationChannel.Socket,
                            Port = port
                        };
                    }
                }
            }

            throw new ArgumentException("Invalid command line communication channel arguments.", nameof(args));
        }
    }
}

davidmatson avatar Aug 09 '21 20:08 davidmatson

Thanks for the code. I have updated the pipe connection for posix platforms (linux/mac) using unix domain sockets, which is what the vs code client is using:

using OmniSharp.Extensions.LanguageServer.Server;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;
using Serilog;

static class LanguageServerOptionsCommandLineExtensions
{
  public static LanguageServerOptions WithCommandLineCommunicationChannel(this LanguageServerOptions options,
      string[] args)
  {
    if (options == null)
    {
      throw new ArgumentNullException(nameof(options));
    }

    CommandLineOptions commandLineOptions = CommandLineOptions.Parse(args);

    NetworkStream? stream;

    switch (commandLineOptions.CommunicationChannel)
    {
      case CommunicationChannel.ConsoleInputOutput:
        options.WithInput(Console.OpenStandardInput());
        options.WithOutput(Console.OpenStandardOutput());
        break;
      case CommunicationChannel.Pipe:
        if (OperatingSystem.IsWindows())
        {
          NamedPipeClientStream pipe = new NamedPipeClientStream(".", commandLineOptions.PipeName,
              PipeDirection.InOut, PipeOptions.Asynchronous);
          // TODO: Make async? How to do that inside LanguageServer.From(Action<LanguageServerOptions>)?
          //await pipe.ConnectAsync(cancellationToken);
          pipe.Connect();
          options.WithInput(pipe);
          options.WithOutput(pipe);
        }
        else
        {
          var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.IP);
          var endpoint = new UnixDomainSocketEndPoint(commandLineOptions.PipeName);
          socket.Connect(endpoint);
          stream = new NetworkStream(socket);

          options.WithInput(stream);
          options.WithOutput(stream);
        }
        break;
      default:
        Debug.Assert(commandLineOptions.CommunicationChannel == CommunicationChannel.Socket);
        TcpClient client = new TcpClient();
        options.RegisterForDisposal(client);
        // TODO: Make async? How to do that inside LanguageServer.From(Action<LanguageServerOptions>)?
        //await client.ConnectAsync(IPAddress.Loopback, commandLineOptions.Port, cancellationToken);
        client.Connect(IPAddress.Loopback, commandLineOptions.Port);
        stream = client.GetStream();
        options.WithInput(stream);
        options.WithOutput(stream);
        break;
    }

    return options;
  }

  enum CommunicationChannel
  {
    ConsoleInputOutput,
    Pipe,
    Socket
  }

  struct CommandLineOptions
  {
    public CommunicationChannel CommunicationChannel { get; init; }

    public string PipeName { get; init; }

    public int Port { get; init; }

    private static string ParsePipeNameWindows(string firstArgument, string? secondArgument)
    {

      if (firstArgument == "--pipe" && secondArgument != null && secondArgument.StartsWith(@"\\.\pipe\"))
      {
        return secondArgument.Substring(@"\\.\pipe\".Length);
      }
      else if (firstArgument.StartsWith(@"--pipe=\\.\pipe\") && secondArgument == null)
      {
        return firstArgument.Substring(@"--pipe=\\.\pipe\".Length);
      }

      throw new Exception("Invalid pipe argument");
    }


    private static string ParsePipeNamePosix(string firstArgument, string? secondArgument)
    {
      if (firstArgument == "--pipe" && secondArgument != null)
      {
        return secondArgument;
      }
      else if (firstArgument.StartsWith(@"--pipe=") && secondArgument == null)
      {
        var sockPath = firstArgument.Substring(@"--pipe=".Length);
        return sockPath;
      }

      throw new Exception("Invalid pipe argument");
    }

    private static string ParsePipeName(string firstArgument, string? secondArgument)
    {
      if (OperatingSystem.IsWindows())
      {
        return ParsePipeNameWindows(firstArgument, secondArgument);
      }
      else
      {
        return ParsePipeNamePosix(firstArgument, secondArgument);
      }
    }

    public static CommandLineOptions Parse(string[] args)
    {
      if (args == null || args.Length == 0)
      {
        return new CommandLineOptions { CommunicationChannel = CommunicationChannel.ConsoleInputOutput };
      }

      if (args.Length <= 2)
      {
        string firstArgument = args[0];
        string? secondArgument = args.Length > 1 ? args[1] : null;

        if (firstArgument == "--stdio" && secondArgument == null)
        {
          return new CommandLineOptions { CommunicationChannel = CommunicationChannel.ConsoleInputOutput };
        }
        else if (firstArgument.StartsWith("--pipe"))
        {
          return new CommandLineOptions
          {
            CommunicationChannel = CommunicationChannel.Pipe,
            PipeName = ParsePipeName(firstArgument, secondArgument)
          };
        }
        else if (firstArgument.StartsWith("--socket"))
        {
          if (firstArgument == "--socket" && secondArgument != null)
          {
            return new CommandLineOptions
            {
              CommunicationChannel = CommunicationChannel.Socket,
              Port = int.Parse(secondArgument, NumberStyles.None, CultureInfo.InvariantCulture)
            };
          }
          else if (firstArgument.StartsWith("--socket=") && secondArgument == null)
          {
            int port = int.Parse(firstArgument.Substring("--socket=".Length), NumberStyles.None,
                CultureInfo.InvariantCulture);
            return new CommandLineOptions
            {
              CommunicationChannel = CommunicationChannel.Socket,
              Port = port
            };
          }
        }
      }

      throw new ArgumentException("Invalid command line communication channel arguments.", nameof(args));
    }
  }
}

koliyo avatar Aug 23 '22 14:08 koliyo