Initial commit of SqlTools Service API

This commit is contained in:
Karl Burtram
2016-07-15 11:02:03 -07:00
parent 874ade9001
commit 790825cfab
112 changed files with 9782 additions and 1 deletions

View File

@@ -0,0 +1,81 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
{
/// <summary>
/// Defines a base implementation for servers and their clients over a
/// single kind of communication channel.
/// </summary>
public abstract class ChannelBase
{
/// <summary>
/// Gets a boolean that is true if the channel is connected or false if not.
/// </summary>
public bool IsConnected { get; protected set; }
/// <summary>
/// Gets the MessageReader for reading messages from the channel.
/// </summary>
public MessageReader MessageReader { get; protected set; }
/// <summary>
/// Gets the MessageWriter for writing messages to the channel.
/// </summary>
public MessageWriter MessageWriter { get; protected set; }
/// <summary>
/// Starts the channel and initializes the MessageDispatcher.
/// </summary>
/// <param name="messageProtocolType">The type of message protocol used by the channel.</param>
public void Start(MessageProtocolType messageProtocolType)
{
IMessageSerializer messageSerializer = null;
if (messageProtocolType == MessageProtocolType.LanguageServer)
{
messageSerializer = new JsonRpcMessageSerializer();
}
else
{
messageSerializer = new V8MessageSerializer();
}
this.Initialize(messageSerializer);
}
/// <summary>
/// Returns a Task that allows the consumer of the ChannelBase
/// implementation to wait until a connection has been made to
/// the opposite endpoint whether it's a client or server.
/// </summary>
/// <returns>A Task to be awaited until a connection is made.</returns>
public abstract Task WaitForConnection();
/// <summary>
/// Stops the channel.
/// </summary>
public void Stop()
{
this.Shutdown();
}
/// <summary>
/// A method to be implemented by subclasses to handle the
/// actual initialization of the channel and the creation and
/// assignment of the MessageReader and MessageWriter properties.
/// </summary>
/// <param name="messageSerializer">The IMessageSerializer to use for message serialization.</param>
protected abstract void Initialize(IMessageSerializer messageSerializer);
/// <summary>
/// A method to be implemented by subclasses to handle shutdown
/// of the channel once Stop is called.
/// </summary>
protected abstract void Shutdown();
}
}

View File

@@ -0,0 +1,83 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
#if false
using System;
using System.IO.Pipes;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
{
public class NamedPipeClientChannel : ChannelBase
{
private string pipeName;
private bool isClientConnected;
private NamedPipeClientStream pipeClient;
public NamedPipeClientChannel(string pipeName)
{
this.pipeName = pipeName;
}
public override async Task WaitForConnection()
{
#if NanoServer
await this.pipeClient.ConnectAsync();
#else
this.IsConnected = false;
while (!this.IsConnected)
{
try
{
// Wait for 500 milliseconds so that we don't tie up the thread
this.pipeClient.Connect(500);
this.IsConnected = this.pipeClient.IsConnected;
}
catch (TimeoutException)
{
// Connect timed out, wait and try again
await Task.Delay(1000);
continue;
}
}
#endif
// If we've reached this point, we're connected
this.IsConnected = true;
}
protected override void Initialize(IMessageSerializer messageSerializer)
{
this.pipeClient =
new NamedPipeClientStream(
".",
this.pipeName,
PipeDirection.InOut,
PipeOptions.Asynchronous);
this.MessageReader =
new MessageReader(
this.pipeClient,
messageSerializer);
this.MessageWriter =
new MessageWriter(
this.pipeClient,
messageSerializer);
}
protected override void Shutdown()
{
if (this.pipeClient != null)
{
this.pipeClient.Dispose();
}
}
}
}
#endif

View File

@@ -0,0 +1,65 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
#if false
using System;
using System.IO.Pipes;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
{
public class NamedPipeServerChannel : ChannelBase
{
private string pipeName;
private NamedPipeServerStream pipeServer;
public NamedPipeServerChannel(string pipeName)
{
this.pipeName = pipeName;
}
public override async Task WaitForConnection()
{
#if NanoServer
await this.pipeServer.WaitForConnectionAsync();
#else
await Task.Factory.FromAsync(this.pipeServer.BeginWaitForConnection, this.pipeServer.EndWaitForConnection, null);
#endif
this.IsConnected = true;
}
protected override void Initialize(IMessageSerializer messageSerializer)
{
this.pipeServer =
new NamedPipeServerStream(
pipeName,
PipeDirection.InOut,
1,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous);
this.MessageReader =
new MessageReader(
this.pipeServer,
messageSerializer);
this.MessageWriter =
new MessageWriter(
this.pipeServer,
messageSerializer);
}
protected override void Shutdown()
{
if (this.pipeServer != null)
{
this.pipeServer.Dispose();
}
}
}
}
#endif

View File

@@ -0,0 +1,127 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers;
using System.Diagnostics;
using System.IO;
using System.Text;
using System;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
{
/// <summary>
/// Provides a client implementation for the standard I/O channel.
/// Launches the server process and then attaches to its console
/// streams.
/// </summary>
public class StdioClientChannel : ChannelBase
{
private string serviceProcessPath;
private string serviceProcessArguments;
private Stream inputStream;
private Stream outputStream;
private Process serviceProcess;
/// <summary>
/// Gets the process ID of the server process.
/// </summary>
public int ProcessId { get; private set; }
/// <summary>
/// Initializes an instance of the StdioClient.
/// </summary>
/// <param name="serverProcessPath">The full path to the server process executable.</param>
/// <param name="serverProcessArguments">Optional arguments to pass to the service process executable.</param>
public StdioClientChannel(
string serverProcessPath,
params string[] serverProcessArguments)
{
this.serviceProcessPath = serverProcessPath;
if (serverProcessArguments != null)
{
this.serviceProcessArguments =
string.Join(
" ",
serverProcessArguments);
}
}
protected override void Initialize(IMessageSerializer messageSerializer)
{
this.serviceProcess = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = this.serviceProcessPath,
Arguments = this.serviceProcessArguments,
CreateNoWindow = true,
UseShellExecute = false,
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
StandardOutputEncoding = Encoding.UTF8,
},
EnableRaisingEvents = true,
};
// Start the process
this.serviceProcess.Start();
this.ProcessId = this.serviceProcess.Id;
// Open the standard input/output streams
this.inputStream = this.serviceProcess.StandardOutput.BaseStream;
this.outputStream = this.serviceProcess.StandardInput.BaseStream;
// Set up the message reader and writer
this.MessageReader =
new MessageReader(
this.inputStream,
messageSerializer);
this.MessageWriter =
new MessageWriter(
this.outputStream,
messageSerializer);
this.IsConnected = true;
}
public override Task WaitForConnection()
{
// We're always connected immediately in the stdio channel
return Task.FromResult(true);
}
protected override void Shutdown()
{
if (this.inputStream != null)
{
this.inputStream.Dispose();
this.inputStream = null;
}
if (this.outputStream != null)
{
this.outputStream.Dispose();
this.outputStream = null;
}
if (this.MessageReader != null)
{
this.MessageReader = null;
}
if (this.MessageWriter != null)
{
this.MessageWriter = null;
}
this.serviceProcess.Kill();
}
}
}

View File

@@ -0,0 +1,62 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers;
using System.IO;
using System.Text;
using System;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel
{
/// <summary>
/// Provides a server implementation for the standard I/O channel.
/// When started in a process, attaches to the console I/O streams
/// to communicate with the client that launched the process.
/// </summary>
public class StdioServerChannel : ChannelBase
{
private Stream inputStream;
private Stream outputStream;
protected override void Initialize(IMessageSerializer messageSerializer)
{
#if !NanoServer
// Ensure that the console is using UTF-8 encoding
System.Console.InputEncoding = Encoding.UTF8;
System.Console.OutputEncoding = Encoding.UTF8;
#endif
// Open the standard input/output streams
this.inputStream = System.Console.OpenStandardInput();
this.outputStream = System.Console.OpenStandardOutput();
// Set up the reader and writer
this.MessageReader =
new MessageReader(
this.inputStream,
messageSerializer);
this.MessageWriter =
new MessageWriter(
this.outputStream,
messageSerializer);
this.IsConnected = true;
}
public override Task WaitForConnection()
{
// We're always connected immediately in the stdio channel
return Task.FromResult(true);
}
protected override void Shutdown()
{
// No default implementation needed, streams will be
// disposed on process shutdown.
}
}
}

View File

@@ -0,0 +1,31 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
public static class Constants
{
public const string ContentLengthFormatString = "Content-Length: {0}\r\n\r\n";
public static readonly JsonSerializerSettings JsonSerializerSettings;
static Constants()
{
JsonSerializerSettings = new JsonSerializerSettings();
// Camel case all object properties
JsonSerializerSettings.ContractResolver =
new CamelCasePropertyNamesContractResolver();
}
}
}

View File

@@ -0,0 +1,34 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
/// <summary>
/// Provides context for a received event so that handlers
/// can write events back to the channel.
/// </summary>
public class EventContext
{
private MessageWriter messageWriter;
public EventContext(MessageWriter messageWriter)
{
this.messageWriter = messageWriter;
}
public async Task SendEvent<TParams>(
EventType<TParams> eventType,
TParams eventParams)
{
await this.messageWriter.WriteEvent(
eventType,
eventParams);
}
}
}

View File

@@ -0,0 +1,33 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
/// <summary>
/// Defines an event type with a particular method name.
/// </summary>
/// <typeparam name="TParams">The parameter type for this event.</typeparam>
public class EventType<TParams>
{
/// <summary>
/// Gets the method name for the event type.
/// </summary>
public string MethodName { get; private set; }
/// <summary>
/// Creates an EventType instance with the given parameter type and method name.
/// </summary>
/// <param name="methodName">The method name of the event.</param>
/// <returns>A new EventType instance for the defined type.</returns>
public static EventType<TParams> Create(string methodName)
{
return new EventType<TParams>()
{
MethodName = methodName
};
}
}
}

View File

@@ -0,0 +1,22 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
internal interface IMessageSender
{
Task SendEvent<TParams>(
EventType<TParams> eventType,
TParams eventParams);
Task<TResult> SendRequest<TParams, TResult>(
RequestType<TParams, TResult> requestType,
TParams requestParams,
bool waitForResponse);
}
}

View File

@@ -0,0 +1,30 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Newtonsoft.Json.Linq;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
/// <summary>
/// Defines a common interface for message serializers.
/// </summary>
public interface IMessageSerializer
{
/// <summary>
/// Serializes a Message to a JObject.
/// </summary>
/// <param name="message">The message to be serialized.</param>
/// <returns>A JObject which contains the JSON representation of the message.</returns>
JObject SerializeMessage(Message message);
/// <summary>
/// Deserializes a JObject to a Messsage.
/// </summary>
/// <param name="messageJson">The JObject containing the JSON representation of the message.</param>
/// <returns>The Message that was represented by the JObject.</returns>
Message DeserializeMessage(JObject messageJson);
}
}

View File

@@ -0,0 +1,136 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Diagnostics;
using Newtonsoft.Json.Linq;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
/// <summary>
/// Defines all possible message types.
/// </summary>
public enum MessageType
{
Unknown,
Request,
Response,
Event
}
/// <summary>
/// Provides common details for protocol messages of any format.
/// </summary>
[DebuggerDisplay("MessageType = {MessageType.ToString()}, Method = {Method}, Id = {Id}")]
public class Message
{
/// <summary>
/// Gets or sets the message type.
/// </summary>
public MessageType MessageType { get; set; }
/// <summary>
/// Gets or sets the message's sequence ID.
/// </summary>
public string Id { get; set; }
/// <summary>
/// Gets or sets the message's method/command name.
/// </summary>
public string Method { get; set; }
/// <summary>
/// Gets or sets a JToken containing the contents of the message.
/// </summary>
public JToken Contents { get; set; }
/// <summary>
/// Gets or sets a JToken containing error details.
/// </summary>
public JToken Error { get; set; }
/// <summary>
/// Creates a message with an Unknown type.
/// </summary>
/// <returns>A message with Unknown type.</returns>
public static Message Unknown()
{
return new Message
{
MessageType = MessageType.Unknown
};
}
/// <summary>
/// Creates a message with a Request type.
/// </summary>
/// <param name="id">The sequence ID of the request.</param>
/// <param name="method">The method name of the request.</param>
/// <param name="contents">The contents of the request.</param>
/// <returns>A message with a Request type.</returns>
public static Message Request(string id, string method, JToken contents)
{
return new Message
{
MessageType = MessageType.Request,
Id = id,
Method = method,
Contents = contents
};
}
/// <summary>
/// Creates a message with a Response type.
/// </summary>
/// <param name="id">The sequence ID of the original request.</param>
/// <param name="method">The method name of the original request.</param>
/// <param name="contents">The contents of the response.</param>
/// <returns>A message with a Response type.</returns>
public static Message Response(string id, string method, JToken contents)
{
return new Message
{
MessageType = MessageType.Response,
Id = id,
Method = method,
Contents = contents
};
}
/// <summary>
/// Creates a message with a Response type and error details.
/// </summary>
/// <param name="id">The sequence ID of the original request.</param>
/// <param name="method">The method name of the original request.</param>
/// <param name="error">The error details of the response.</param>
/// <returns>A message with a Response type and error details.</returns>
public static Message ResponseError(string id, string method, JToken error)
{
return new Message
{
MessageType = MessageType.Response,
Id = id,
Method = method,
Error = error
};
}
/// <summary>
/// Creates a message with an Event type.
/// </summary>
/// <param name="method">The method name of the event.</param>
/// <param name="contents">The contents of the event.</param>
/// <returns>A message with an Event type.</returns>
public static Message Event(string method, JToken contents)
{
return new Message
{
MessageType = MessageType.Event,
Method = method,
Contents = contents
};
}
}
}

View File

@@ -0,0 +1,325 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
using Microsoft.PowerShell.EditorServices.Utility;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
public class MessageDispatcher
{
#region Fields
private ChannelBase protocolChannel;
// private AsyncQueue<Message> messagesToWrite;
private AsyncContextThread messageLoopThread;
private Dictionary<string, Func<Message, MessageWriter, Task>> requestHandlers =
new Dictionary<string, Func<Message, MessageWriter, Task>>();
private Dictionary<string, Func<Message, MessageWriter, Task>> eventHandlers =
new Dictionary<string, Func<Message, MessageWriter, Task>>();
private Action<Message> responseHandler;
private CancellationTokenSource messageLoopCancellationToken =
new CancellationTokenSource();
#endregion
#region Properties
public SynchronizationContext SynchronizationContext { get; private set; }
public bool InMessageLoopThread
{
get
{
// We're in the same thread as the message loop if the
// current synchronization context equals the one we
// know.
return SynchronizationContext.Current == this.SynchronizationContext;
}
}
protected MessageReader MessageReader { get; private set; }
protected MessageWriter MessageWriter { get; private set; }
#endregion
#region Constructors
public MessageDispatcher(ChannelBase protocolChannel)
{
this.protocolChannel = protocolChannel;
this.MessageReader = protocolChannel.MessageReader;
this.MessageWriter = protocolChannel.MessageWriter;
}
#endregion
#region Public Methods
public void Start()
{
// Start the main message loop thread. The Task is
// not explicitly awaited because it is running on
// an independent background thread.
this.messageLoopThread = new AsyncContextThread("Message Dispatcher");
this.messageLoopThread
.Run(() => this.ListenForMessages(this.messageLoopCancellationToken.Token))
.ContinueWith(this.OnListenTaskCompleted);
}
public void Stop()
{
// Stop the message loop thread
if (this.messageLoopThread != null)
{
this.messageLoopCancellationToken.Cancel();
this.messageLoopThread.Stop();
}
}
public void SetRequestHandler<TParams, TResult>(
RequestType<TParams, TResult> requestType,
Func<TParams, RequestContext<TResult>, Task> requestHandler)
{
this.SetRequestHandler(
requestType,
requestHandler,
false);
}
public void SetRequestHandler<TParams, TResult>(
RequestType<TParams, TResult> requestType,
Func<TParams, RequestContext<TResult>, Task> requestHandler,
bool overrideExisting)
{
if (overrideExisting)
{
// Remove the existing handler so a new one can be set
this.requestHandlers.Remove(requestType.MethodName);
}
this.requestHandlers.Add(
requestType.MethodName,
(requestMessage, messageWriter) =>
{
var requestContext =
new RequestContext<TResult>(
requestMessage,
messageWriter);
TParams typedParams = default(TParams);
if (requestMessage.Contents != null)
{
// TODO: Catch parse errors!
typedParams = requestMessage.Contents.ToObject<TParams>();
}
return requestHandler(typedParams, requestContext);
});
}
public void SetEventHandler<TParams>(
EventType<TParams> eventType,
Func<TParams, EventContext, Task> eventHandler)
{
this.SetEventHandler(
eventType,
eventHandler,
false);
}
public void SetEventHandler<TParams>(
EventType<TParams> eventType,
Func<TParams, EventContext, Task> eventHandler,
bool overrideExisting)
{
if (overrideExisting)
{
// Remove the existing handler so a new one can be set
this.eventHandlers.Remove(eventType.MethodName);
}
this.eventHandlers.Add(
eventType.MethodName,
(eventMessage, messageWriter) =>
{
var eventContext = new EventContext(messageWriter);
TParams typedParams = default(TParams);
if (eventMessage.Contents != null)
{
// TODO: Catch parse errors!
typedParams = eventMessage.Contents.ToObject<TParams>();
}
return eventHandler(typedParams, eventContext);
});
}
public void SetResponseHandler(Action<Message> responseHandler)
{
this.responseHandler = responseHandler;
}
#endregion
#region Events
public event EventHandler<Exception> UnhandledException;
protected void OnUnhandledException(Exception unhandledException)
{
if (this.UnhandledException != null)
{
this.UnhandledException(this, unhandledException);
}
}
#endregion
#region Private Methods
private async Task ListenForMessages(CancellationToken cancellationToken)
{
this.SynchronizationContext = SynchronizationContext.Current;
// Run the message loop
bool isRunning = true;
while (isRunning && !cancellationToken.IsCancellationRequested)
{
Message newMessage = null;
try
{
// Read a message from the channel
newMessage = await this.MessageReader.ReadMessage();
}
catch (MessageParseException e)
{
// TODO: Write an error response
Logger.Write(
LogLevel.Error,
"Could not parse a message that was received:\r\n\r\n" +
e.ToString());
// Continue the loop
continue;
}
catch (EndOfStreamException)
{
// The stream has ended, end the message loop
break;
}
catch (Exception e)
{
var b = e.Message;
newMessage = null;
}
// The message could be null if there was an error parsing the
// previous message. In this case, do not try to dispatch it.
if (newMessage != null)
{
// Process the message
await this.DispatchMessage(
newMessage,
this.MessageWriter);
}
}
}
protected async Task DispatchMessage(
Message messageToDispatch,
MessageWriter messageWriter)
{
Task handlerToAwait = null;
if (messageToDispatch.MessageType == MessageType.Request)
{
Func<Message, MessageWriter, Task> requestHandler = null;
if (this.requestHandlers.TryGetValue(messageToDispatch.Method, out requestHandler))
{
handlerToAwait = requestHandler(messageToDispatch, messageWriter);
}
else
{
// TODO: Message not supported error
}
}
else if (messageToDispatch.MessageType == MessageType.Response)
{
if (this.responseHandler != null)
{
this.responseHandler(messageToDispatch);
}
}
else if (messageToDispatch.MessageType == MessageType.Event)
{
Func<Message, MessageWriter, Task> eventHandler = null;
if (this.eventHandlers.TryGetValue(messageToDispatch.Method, out eventHandler))
{
handlerToAwait = eventHandler(messageToDispatch, messageWriter);
}
else
{
// TODO: Message not supported error
}
}
else
{
// TODO: Return message not supported
}
if (handlerToAwait != null)
{
try
{
await handlerToAwait;
}
catch (TaskCanceledException)
{
// Some tasks may be cancelled due to legitimate
// timeouts so don't let those exceptions go higher.
}
catch (AggregateException e)
{
if (!(e.InnerExceptions[0] is TaskCanceledException))
{
// Cancelled tasks aren't a problem, so rethrow
// anything that isn't a TaskCanceledException
throw e;
}
}
}
}
private void OnListenTaskCompleted(Task listenTask)
{
if (listenTask.IsFaulted)
{
this.OnUnhandledException(listenTask.Exception);
}
else if (listenTask.IsCompleted || listenTask.IsCanceled)
{
// TODO: Dispose of anything?
}
}
#endregion
}
}

View File

@@ -0,0 +1,23 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
public class MessageParseException : Exception
{
public string OriginalMessageText { get; private set; }
public MessageParseException(
string originalMessageText,
string errorMessage,
params object[] errorMessageArgs)
: base(string.Format(errorMessage, errorMessageArgs))
{
this.OriginalMessageText = originalMessageText;
}
}
}

View File

@@ -0,0 +1,23 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
/// <summary>
/// Defines the possible message protocol types.
/// </summary>
public enum MessageProtocolType
{
/// <summary>
/// Identifies the language server message protocol.
/// </summary>
LanguageServer,
/// <summary>
/// Identifies the debug adapter message protocol.
/// </summary>
DebugAdapter
}
}

View File

@@ -0,0 +1,262 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
public class MessageReader
{
#region Private Fields
public const int DefaultBufferSize = 8192;
public const double BufferResizeTrigger = 0.25;
private const int CR = 0x0D;
private const int LF = 0x0A;
private static string[] NewLineDelimiters = new string[] { Environment.NewLine };
private Stream inputStream;
private IMessageSerializer messageSerializer;
private Encoding messageEncoding;
private ReadState readState;
private bool needsMoreData = true;
private int readOffset;
private int bufferEndOffset;
private byte[] messageBuffer = new byte[DefaultBufferSize];
private int expectedContentLength;
private Dictionary<string, string> messageHeaders;
enum ReadState
{
Headers,
Content
}
#endregion
#region Constructors
public MessageReader(
Stream inputStream,
IMessageSerializer messageSerializer,
Encoding messageEncoding = null)
{
Validate.IsNotNull("streamReader", inputStream);
Validate.IsNotNull("messageSerializer", messageSerializer);
this.inputStream = inputStream;
this.messageSerializer = messageSerializer;
this.messageEncoding = messageEncoding;
if (messageEncoding == null)
{
this.messageEncoding = Encoding.UTF8;
}
this.messageBuffer = new byte[DefaultBufferSize];
}
#endregion
#region Public Methods
public async Task<Message> ReadMessage()
{
string messageContent = null;
// Do we need to read more data or can we process the existing buffer?
while (!this.needsMoreData || await this.ReadNextChunk())
{
// Clear the flag since we should have what we need now
this.needsMoreData = false;
// Do we need to look for message headers?
if (this.readState == ReadState.Headers &&
!this.TryReadMessageHeaders())
{
// If we don't have enough data to read headers yet, keep reading
this.needsMoreData = true;
continue;
}
// Do we need to look for message content?
if (this.readState == ReadState.Content &&
!this.TryReadMessageContent(out messageContent))
{
// If we don't have enough data yet to construct the content, keep reading
this.needsMoreData = true;
continue;
}
// We've read a message now, break out of the loop
break;
}
// Get the JObject for the JSON content
JObject messageObject = JObject.Parse(messageContent);
// Load the message
Logger.Write(
LogLevel.Verbose,
string.Format(
"READ MESSAGE:\r\n\r\n{0}",
messageObject.ToString(Formatting.Indented)));
// Return the parsed message
return this.messageSerializer.DeserializeMessage(messageObject);
}
#endregion
#region Private Methods
private async Task<bool> ReadNextChunk()
{
// Do we need to resize the buffer? See if less than 1/4 of the space is left.
if (((double)(this.messageBuffer.Length - this.bufferEndOffset) / this.messageBuffer.Length) < 0.25)
{
// Double the size of the buffer
Array.Resize(
ref this.messageBuffer,
this.messageBuffer.Length * 2);
}
// Read the next chunk into the message buffer
int readLength =
await this.inputStream.ReadAsync(
this.messageBuffer,
this.bufferEndOffset,
this.messageBuffer.Length - this.bufferEndOffset);
this.bufferEndOffset += readLength;
if (readLength == 0)
{
// If ReadAsync returns 0 then it means that the stream was
// closed unexpectedly (usually due to the client application
// ending suddenly). For now, just terminate the language
// server immediately.
// TODO: Provide a more graceful shutdown path
throw new EndOfStreamException(
"MessageReader's input stream ended unexpectedly, terminating.");
}
return true;
}
private bool TryReadMessageHeaders()
{
int scanOffset = this.readOffset;
// Scan for the final double-newline that marks the
// end of the header lines
while (scanOffset + 3 < this.bufferEndOffset &&
(this.messageBuffer[scanOffset] != CR ||
this.messageBuffer[scanOffset + 1] != LF ||
this.messageBuffer[scanOffset + 2] != CR ||
this.messageBuffer[scanOffset + 3] != LF))
{
scanOffset++;
}
// No header or body separator found (e.g CRLFCRLF)
if (scanOffset + 3 >= this.bufferEndOffset)
{
return false;
}
this.messageHeaders = new Dictionary<string, string>();
var headers =
Encoding.ASCII
.GetString(this.messageBuffer, this.readOffset, scanOffset)
.Split(NewLineDelimiters, StringSplitOptions.RemoveEmptyEntries);
// Read each header and store it in the dictionary
foreach (var header in headers)
{
int currentLength = header.IndexOf(':');
if (currentLength == -1)
{
throw new ArgumentException("Message header must separate key and value using :");
}
var key = header.Substring(0, currentLength);
var value = header.Substring(currentLength + 1).Trim();
this.messageHeaders[key] = value;
}
// Make sure a Content-Length header was present, otherwise it
// is a fatal error
string contentLengthString = null;
if (!this.messageHeaders.TryGetValue("Content-Length", out contentLengthString))
{
throw new MessageParseException("", "Fatal error: Content-Length header must be provided.");
}
// Parse the content length to an integer
if (!int.TryParse(contentLengthString, out this.expectedContentLength))
{
throw new MessageParseException("", "Fatal error: Content-Length value is not an integer.");
}
// Skip past the headers plus the newline characters
this.readOffset += scanOffset + 4;
// Done reading headers, now read content
this.readState = ReadState.Content;
return true;
}
private bool TryReadMessageContent(out string messageContent)
{
messageContent = null;
// Do we have enough bytes to reach the expected length?
if ((this.bufferEndOffset - this.readOffset) < this.expectedContentLength)
{
return false;
}
// Convert the message contents to a string using the specified encoding
messageContent =
this.messageEncoding.GetString(
this.messageBuffer,
this.readOffset,
this.expectedContentLength);
// Move the remaining bytes to the front of the buffer for the next message
var remainingByteCount = this.bufferEndOffset - (this.expectedContentLength + this.readOffset);
Buffer.BlockCopy(
this.messageBuffer,
this.expectedContentLength + this.readOffset,
this.messageBuffer,
0,
remainingByteCount);
// Reset the offsets for the next read
this.readOffset = 0;
this.bufferEndOffset = remainingByteCount;
// Done reading content, now look for headers
this.readState = ReadState.Headers;
return true;
}
#endregion
}
}

View File

@@ -0,0 +1,141 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using System;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
public class MessageWriter
{
#region Private Fields
private Stream outputStream;
private IMessageSerializer messageSerializer;
private AsyncLock writeLock = new AsyncLock();
private JsonSerializer contentSerializer =
JsonSerializer.Create(
Constants.JsonSerializerSettings);
#endregion
#region Constructors
public MessageWriter(
Stream outputStream,
IMessageSerializer messageSerializer)
{
Validate.IsNotNull("streamWriter", outputStream);
Validate.IsNotNull("messageSerializer", messageSerializer);
this.outputStream = outputStream;
this.messageSerializer = messageSerializer;
}
#endregion
#region Public Methods
// TODO: This method should be made protected or private
public async Task WriteMessage(Message messageToWrite)
{
Validate.IsNotNull("messageToWrite", messageToWrite);
// Serialize the message
JObject messageObject =
this.messageSerializer.SerializeMessage(
messageToWrite);
// Log the JSON representation of the message
Logger.Write(
LogLevel.Verbose,
string.Format(
"WRITE MESSAGE:\r\n\r\n{0}",
JsonConvert.SerializeObject(
messageObject,
Formatting.Indented,
Constants.JsonSerializerSettings)));
string serializedMessage =
JsonConvert.SerializeObject(
messageObject,
Constants.JsonSerializerSettings);
byte[] messageBytes = Encoding.UTF8.GetBytes(serializedMessage);
byte[] headerBytes =
Encoding.ASCII.GetBytes(
string.Format(
Constants.ContentLengthFormatString,
messageBytes.Length));
// Make sure only one call is writing at a time. You might be thinking
// "Why not use a normal lock?" We use an AsyncLock here so that the
// message loop doesn't get blocked while waiting for I/O to complete.
using (await this.writeLock.LockAsync())
{
// Send the message
await this.outputStream.WriteAsync(headerBytes, 0, headerBytes.Length);
await this.outputStream.WriteAsync(messageBytes, 0, messageBytes.Length);
await this.outputStream.FlushAsync();
}
}
public async Task WriteRequest<TParams, TResult>(
RequestType<TParams, TResult> requestType,
TParams requestParams,
int requestId)
{
// Allow null content
JToken contentObject =
requestParams != null ?
JToken.FromObject(requestParams, contentSerializer) :
null;
await this.WriteMessage(
Message.Request(
requestId.ToString(),
requestType.MethodName,
contentObject));
}
public async Task WriteResponse<TResult>(TResult resultContent, string method, string requestId)
{
// Allow null content
JToken contentObject =
resultContent != null ?
JToken.FromObject(resultContent, contentSerializer) :
null;
await this.WriteMessage(
Message.Response(
requestId,
method,
contentObject));
}
public async Task WriteEvent<TParams>(EventType<TParams> eventType, TParams eventParams)
{
// Allow null content
JToken contentObject =
eventParams != null ?
JToken.FromObject(eventParams, contentSerializer) :
null;
await this.WriteMessage(
Message.Event(
eventType.MethodName,
contentObject));
}
#endregion
}
}

View File

@@ -0,0 +1,313 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Channel;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
/// <summary>
/// Provides behavior for a client or server endpoint that
/// communicates using the specified protocol.
/// </summary>
public class ProtocolEndpoint : IMessageSender
{
private bool isStarted;
private int currentMessageId;
private ChannelBase protocolChannel;
private MessageProtocolType messageProtocolType;
private TaskCompletionSource<bool> endpointExitedTask;
private SynchronizationContext originalSynchronizationContext;
private Dictionary<string, TaskCompletionSource<Message>> pendingRequests =
new Dictionary<string, TaskCompletionSource<Message>>();
/// <summary>
/// Gets the MessageDispatcher which allows registration of
/// handlers for requests, responses, and events that are
/// transmitted through the channel.
/// </summary>
protected MessageDispatcher MessageDispatcher { get; set; }
/// <summary>
/// Initializes an instance of the protocol server using the
/// specified channel for communication.
/// </summary>
/// <param name="protocolChannel">
/// The channel to use for communication with the connected endpoint.
/// </param>
/// <param name="messageProtocolType">
/// The type of message protocol used by the endpoint.
/// </param>
public ProtocolEndpoint(
ChannelBase protocolChannel,
MessageProtocolType messageProtocolType)
{
this.protocolChannel = protocolChannel;
this.messageProtocolType = messageProtocolType;
this.originalSynchronizationContext = SynchronizationContext.Current;
}
/// <summary>
/// Starts the language server client and sends the Initialize method.
/// </summary>
/// <returns>A Task that can be awaited for initialization to complete.</returns>
public async Task Start()
{
if (!this.isStarted)
{
// Start the provided protocol channel
this.protocolChannel.Start(this.messageProtocolType);
// Start the message dispatcher
this.MessageDispatcher = new MessageDispatcher(this.protocolChannel);
// Set the handler for any message responses that come back
this.MessageDispatcher.SetResponseHandler(this.HandleResponse);
// Listen for unhandled exceptions from the dispatcher
this.MessageDispatcher.UnhandledException += MessageDispatcher_UnhandledException;
// Notify implementation about endpoint start
await this.OnStart();
// Wait for connection and notify the implementor
// NOTE: This task is not meant to be awaited.
Task waitTask =
this.protocolChannel
.WaitForConnection()
.ContinueWith(
async (t) =>
{
// Start the MessageDispatcher
this.MessageDispatcher.Start();
await this.OnConnect();
});
// Endpoint is now started
this.isStarted = true;
}
}
public void WaitForExit()
{
this.endpointExitedTask = new TaskCompletionSource<bool>();
this.endpointExitedTask.Task.Wait();
}
public async Task Stop()
{
if (this.isStarted)
{
// Make sure no future calls try to stop the endpoint during shutdown
this.isStarted = false;
// Stop the implementation first
await this.OnStop();
// Stop the dispatcher and channel
this.MessageDispatcher.Stop();
this.protocolChannel.Stop();
// Notify anyone waiting for exit
if (this.endpointExitedTask != null)
{
this.endpointExitedTask.SetResult(true);
}
}
}
#region Message Sending
/// <summary>
/// Sends a request to the server
/// </summary>
/// <typeparam name="TParams"></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="requestType"></param>
/// <param name="requestParams"></param>
/// <returns></returns>
public Task<TResult> SendRequest<TParams, TResult>(
RequestType<TParams, TResult> requestType,
TParams requestParams)
{
return this.SendRequest(requestType, requestParams, true);
}
public async Task<TResult> SendRequest<TParams, TResult>(
RequestType<TParams, TResult> requestType,
TParams requestParams,
bool waitForResponse)
{
if (!this.protocolChannel.IsConnected)
{
throw new InvalidOperationException("SendRequest called when ProtocolChannel was not yet connected");
}
this.currentMessageId++;
TaskCompletionSource<Message> responseTask = null;
if (waitForResponse)
{
responseTask = new TaskCompletionSource<Message>();
this.pendingRequests.Add(
this.currentMessageId.ToString(),
responseTask);
}
await this.protocolChannel.MessageWriter.WriteRequest<TParams, TResult>(
requestType,
requestParams,
this.currentMessageId);
if (responseTask != null)
{
var responseMessage = await responseTask.Task;
return
responseMessage.Contents != null ?
responseMessage.Contents.ToObject<TResult>() :
default(TResult);
}
else
{
// TODO: Better default value here?
return default(TResult);
}
}
/// <summary>
/// Sends an event to the channel's endpoint.
/// </summary>
/// <typeparam name="TParams">The event parameter type.</typeparam>
/// <param name="eventType">The type of event being sent.</param>
/// <param name="eventParams">The event parameters being sent.</param>
/// <returns>A Task that tracks completion of the send operation.</returns>
public Task SendEvent<TParams>(
EventType<TParams> eventType,
TParams eventParams)
{
if (!this.protocolChannel.IsConnected)
{
throw new InvalidOperationException("SendEvent called when ProtocolChannel was not yet connected");
}
// Some events could be raised from a different thread.
// To ensure that messages are written serially, dispatch
// dispatch the SendEvent call to the message loop thread.
if (!this.MessageDispatcher.InMessageLoopThread)
{
TaskCompletionSource<bool> writeTask = new TaskCompletionSource<bool>();
this.MessageDispatcher.SynchronizationContext.Post(
async (obj) =>
{
await this.protocolChannel.MessageWriter.WriteEvent(
eventType,
eventParams);
writeTask.SetResult(true);
}, null);
return writeTask.Task;
}
else
{
return this.protocolChannel.MessageWriter.WriteEvent(
eventType,
eventParams);
}
}
#endregion
#region Message Handling
public void SetRequestHandler<TParams, TResult>(
RequestType<TParams, TResult> requestType,
Func<TParams, RequestContext<TResult>, Task> requestHandler)
{
this.MessageDispatcher.SetRequestHandler(
requestType,
requestHandler);
}
public void SetEventHandler<TParams>(
EventType<TParams> eventType,
Func<TParams, EventContext, Task> eventHandler)
{
this.MessageDispatcher.SetEventHandler(
eventType,
eventHandler,
false);
}
public void SetEventHandler<TParams>(
EventType<TParams> eventType,
Func<TParams, EventContext, Task> eventHandler,
bool overrideExisting)
{
this.MessageDispatcher.SetEventHandler(
eventType,
eventHandler,
overrideExisting);
}
private void HandleResponse(Message responseMessage)
{
TaskCompletionSource<Message> pendingRequestTask = null;
if (this.pendingRequests.TryGetValue(responseMessage.Id, out pendingRequestTask))
{
pendingRequestTask.SetResult(responseMessage);
this.pendingRequests.Remove(responseMessage.Id);
}
}
#endregion
#region Subclass Lifetime Methods
protected virtual Task OnStart()
{
return Task.FromResult(true);
}
protected virtual Task OnConnect()
{
return Task.FromResult(true);
}
protected virtual Task OnStop()
{
return Task.FromResult(true);
}
#endregion
#region Event Handlers
private void MessageDispatcher_UnhandledException(object sender, Exception e)
{
if (this.endpointExitedTask != null)
{
this.endpointExitedTask.SetException(e);
}
else if (this.originalSynchronizationContext != null)
{
this.originalSynchronizationContext.Post(o => { throw e; }, null);
}
}
#endregion
}
}

View File

@@ -0,0 +1,47 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
public class RequestContext<TResult>
{
private Message requestMessage;
private MessageWriter messageWriter;
public RequestContext(Message requestMessage, MessageWriter messageWriter)
{
this.requestMessage = requestMessage;
this.messageWriter = messageWriter;
}
public async Task SendResult(TResult resultDetails)
{
await this.messageWriter.WriteResponse<TResult>(
resultDetails,
requestMessage.Method,
requestMessage.Id);
}
public async Task SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams)
{
await this.messageWriter.WriteEvent(
eventType,
eventParams);
}
public async Task SendError(object errorDetails)
{
await this.messageWriter.WriteMessage(
Message.ResponseError(
requestMessage.Id,
requestMessage.Method,
JToken.FromObject(errorDetails)));
}
}
}

View File

@@ -0,0 +1,24 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Diagnostics;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol
{
[DebuggerDisplay("RequestType MethodName = {MethodName}")]
public class RequestType<TParams, TResult>
{
public string MethodName { get; private set; }
public static RequestType<TParams, TResult> Create(string typeName)
{
return new RequestType<TParams, TResult>()
{
MethodName = typeName
};
}
}
}

View File

@@ -0,0 +1,102 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers
{
/// <summary>
/// Serializes messages in the JSON RPC format. Used primarily
/// for language servers.
/// </summary>
public class JsonRpcMessageSerializer : IMessageSerializer
{
public JObject SerializeMessage(Message message)
{
JObject messageObject = new JObject();
messageObject.Add("jsonrpc", JToken.FromObject("2.0"));
if (message.MessageType == MessageType.Request)
{
messageObject.Add("id", JToken.FromObject(message.Id));
messageObject.Add("method", message.Method);
messageObject.Add("params", message.Contents);
}
else if (message.MessageType == MessageType.Event)
{
messageObject.Add("method", message.Method);
messageObject.Add("params", message.Contents);
}
else if (message.MessageType == MessageType.Response)
{
messageObject.Add("id", JToken.FromObject(message.Id));
if (message.Error != null)
{
// Write error
messageObject.Add("error", message.Error);
}
else
{
// Write result
messageObject.Add("result", message.Contents);
}
}
return messageObject;
}
public Message DeserializeMessage(JObject messageJson)
{
// TODO: Check for jsonrpc version
JToken token = null;
if (messageJson.TryGetValue("id", out token))
{
// Message is a Request or Response
string messageId = token.ToString();
if (messageJson.TryGetValue("result", out token))
{
return Message.Response(messageId, null, token);
}
else if (messageJson.TryGetValue("error", out token))
{
return Message.ResponseError(messageId, null, token);
}
else
{
JToken messageParams = null;
messageJson.TryGetValue("params", out messageParams);
if (!messageJson.TryGetValue("method", out token))
{
// TODO: Throw parse error
}
return Message.Request(messageId, token.ToString(), messageParams);
}
}
else
{
// Messages without an id are events
JToken messageParams = token;
messageJson.TryGetValue("params", out messageParams);
if (!messageJson.TryGetValue("method", out token))
{
// TODO: Throw parse error
}
return Message.Event(token.ToString(), messageParams);
}
}
}
}

View File

@@ -0,0 +1,115 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
namespace Microsoft.PowerShell.EditorServices.Protocol.MessageProtocol.Serializers
{
/// <summary>
/// Serializes messages in the V8 format. Used primarily for debug adapters.
/// </summary>
public class V8MessageSerializer : IMessageSerializer
{
public JObject SerializeMessage(Message message)
{
JObject messageObject = new JObject();
if (message.MessageType == MessageType.Request)
{
messageObject.Add("type", JToken.FromObject("request"));
messageObject.Add("seq", JToken.FromObject(message.Id));
messageObject.Add("command", message.Method);
messageObject.Add("arguments", message.Contents);
}
else if (message.MessageType == MessageType.Event)
{
messageObject.Add("type", JToken.FromObject("event"));
messageObject.Add("event", message.Method);
messageObject.Add("body", message.Contents);
}
else if (message.MessageType == MessageType.Response)
{
messageObject.Add("type", JToken.FromObject("response"));
messageObject.Add("request_seq", JToken.FromObject(message.Id));
messageObject.Add("command", message.Method);
if (message.Error != null)
{
// Write error
messageObject.Add("success", JToken.FromObject(false));
messageObject.Add("message", message.Error);
}
else
{
// Write result
messageObject.Add("success", JToken.FromObject(true));
messageObject.Add("body", message.Contents);
}
}
return messageObject;
}
public Message DeserializeMessage(JObject messageJson)
{
JToken token = null;
if (messageJson.TryGetValue("type", out token))
{
string messageType = token.ToString();
if (string.Equals("request", messageType, StringComparison.CurrentCultureIgnoreCase))
{
return Message.Request(
messageJson.GetValue("seq").ToString(),
messageJson.GetValue("command").ToString(),
messageJson.GetValue("arguments"));
}
else if (string.Equals("response", messageType, StringComparison.CurrentCultureIgnoreCase))
{
if (messageJson.TryGetValue("success", out token))
{
// Was the response for a successful request?
if (token.ToObject<bool>() == true)
{
return Message.Response(
messageJson.GetValue("request_seq").ToString(),
messageJson.GetValue("command").ToString(),
messageJson.GetValue("body"));
}
else
{
return Message.ResponseError(
messageJson.GetValue("request_seq").ToString(),
messageJson.GetValue("command").ToString(),
messageJson.GetValue("message"));
}
}
else
{
// TODO: Parse error
}
}
else if (string.Equals("event", messageType, StringComparison.CurrentCultureIgnoreCase))
{
return Message.Event(
messageJson.GetValue("event").ToString(),
messageJson.GetValue("body"));
}
else
{
return Message.Unknown();
}
}
return Message.Unknown();
}
}
}