mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-25 17:24:17 -05:00
Initial commit of SqlTools Service API
This commit is contained in:
81
ServiceHost/MessageProtocol/Channel/ChannelBase.cs
Normal file
81
ServiceHost/MessageProtocol/Channel/ChannelBase.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
127
ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs
Normal file
127
ServiceHost/MessageProtocol/Channel/StdioClientChannel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
62
ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs
Normal file
62
ServiceHost/MessageProtocol/Channel/StdioServerChannel.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
31
ServiceHost/MessageProtocol/Constants.cs
Normal file
31
ServiceHost/MessageProtocol/Constants.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
34
ServiceHost/MessageProtocol/EventContext.cs
Normal file
34
ServiceHost/MessageProtocol/EventContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
33
ServiceHost/MessageProtocol/EventType.cs
Normal file
33
ServiceHost/MessageProtocol/EventType.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
ServiceHost/MessageProtocol/IMessageSender.cs
Normal file
22
ServiceHost/MessageProtocol/IMessageSender.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
30
ServiceHost/MessageProtocol/IMessageSerializer.cs
Normal file
30
ServiceHost/MessageProtocol/IMessageSerializer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
136
ServiceHost/MessageProtocol/Message.cs
Normal file
136
ServiceHost/MessageProtocol/Message.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
325
ServiceHost/MessageProtocol/MessageDispatcher.cs
Normal file
325
ServiceHost/MessageProtocol/MessageDispatcher.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
23
ServiceHost/MessageProtocol/MessageParseException.cs
Normal file
23
ServiceHost/MessageProtocol/MessageParseException.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
ServiceHost/MessageProtocol/MessageProtocolType.cs
Normal file
23
ServiceHost/MessageProtocol/MessageProtocolType.cs
Normal 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
|
||||
}
|
||||
}
|
||||
262
ServiceHost/MessageProtocol/MessageReader.cs
Normal file
262
ServiceHost/MessageProtocol/MessageReader.cs
Normal 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
|
||||
}
|
||||
}
|
||||
141
ServiceHost/MessageProtocol/MessageWriter.cs
Normal file
141
ServiceHost/MessageProtocol/MessageWriter.cs
Normal 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
|
||||
}
|
||||
}
|
||||
313
ServiceHost/MessageProtocol/ProtocolEndpoint.cs
Normal file
313
ServiceHost/MessageProtocol/ProtocolEndpoint.cs
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
47
ServiceHost/MessageProtocol/RequestContext.cs
Normal file
47
ServiceHost/MessageProtocol/RequestContext.cs
Normal 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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
24
ServiceHost/MessageProtocol/RequestType.cs
Normal file
24
ServiceHost/MessageProtocol/RequestType.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
115
ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs
Normal file
115
ServiceHost/MessageProtocol/Serializers/V8MessageSerializer.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user