Move unused forked code to external directory (#1192)

* Move unused forked code to external directory

* Fix SLN build errors

* Add back resource provider core since it's referenced by main resource provider project

* Update PackageProjects step of pipeline
This commit is contained in:
Karl Burtram
2021-04-16 15:33:35 -07:00
committed by GitHub
parent dc6555a823
commit ccf95aed77
229 changed files with 10058 additions and 10124 deletions

View File

@@ -0,0 +1,32 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Concurrent;
using Microsoft.SqlTools.Hosting.Contracts;
namespace Microsoft.SqlTools.Hosting.Protocol
{
/// <summary>
/// Provides context for a received event so that handlers
/// can write events back to the channel.
/// </summary>
public class EventContext : IEventSender
{
internal readonly BlockingCollection<Message> messageQueue;
public EventContext(BlockingCollection<Message> outgoingMessageQueue)
{
// TODO: Either 1) make this constructor internal and provide a test framework for validating
// or 2) extract an interface for eventcontext to allow users to mock
messageQueue = outgoingMessageQueue;
}
public void SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams)
{
messageQueue.Add(Message.CreateEvent(eventType, eventParams));
}
}
}

View File

@@ -0,0 +1,35 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using Microsoft.SqlTools.Hosting.v2;
namespace Microsoft.SqlTools.Hosting.Protocol
{
/// <summary>
/// Exception thrown when parsing a message from input stream fails.
/// </summary>
public class MessageParseException : Exception
{
public string OriginalMessageText { get; }
public MessageParseException(string originalMessageText, string errorMessage, params object[] errorMessageArgs)
: base(string.Format(errorMessage, errorMessageArgs))
{
OriginalMessageText = originalMessageText;
}
}
/// <summary>
/// Exception thrown when a handler for a given request/event method does not exist
/// </summary>
public class MethodHandlerDoesNotExistException : Exception
{
public MethodHandlerDoesNotExistException(MessageType type, string method)
: base(SR.HostingMethodHandlerDoesNotExist(type.ToString(), method))
{
}
}
}

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 Microsoft.SqlTools.Hosting.Contracts;
namespace Microsoft.SqlTools.Hosting.Protocol
{
/// <summary>
/// Interface for objects that can send events via the JSON RPC channel
/// </summary>
public interface IEventSender
{
/// <summary>
/// Sends an event over the JSON RPC channel
/// </summary>
/// <param name="eventType">Configuration of the event to send</param>
/// <param name="eventParams">Parameters for the event to send</param>
/// <typeparam name="TParams">Type of the parameters for the event, defined in <paramref name="eventType"/></typeparam>
void SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams);
}
}

View File

@@ -0,0 +1,28 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.Hosting.Protocol
{
/// <summary>
/// Interface for a JSON RPC host
/// </summary>
public interface IJsonRpcHost : IEventSender, IRequestSender, IMessageDispatcher
{
/// <summary>
/// Starts the JSON RPC host
/// </summary>
void Start();
/// <summary>
/// Stops the JSON RPC host
/// </summary>
void Stop();
/// <summary>
/// Waits for the JSON RPC host to exit
/// </summary>
void WaitForExit();
}
}

View File

@@ -0,0 +1,72 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Contracts;
namespace Microsoft.SqlTools.Hosting.Protocol
{
/// <summary>
/// Interface for objects that can will handle messages. The methods exposed via this interface
/// allow users to what to do when a specific message is received.
/// </summary>
public interface IMessageDispatcher
{
/// <summary>
/// Sets the function to run when a request message of a specific
/// <paramref name="requestType"/> is received
/// </summary>
/// <param name="requestType">Configuration of the request message <paramref name="requestHandler"/> will handle</param>
/// <param name="requestHandler">What to do when a request message of <paramref name="requestType"/> is received</param>
/// <param name="overrideExisting">If <c>true</c>, any existing handler will be replaced with this one</param>
/// <typeparam name="TParams">Type of the parameters for the request, defined by <paramref name="requestType"/></typeparam>
/// <typeparam name="TResult">Type of the response to the request, defined by <paramref name="requestType"/></typeparam>
void SetAsyncRequestHandler<TParams, TResult>(
RequestType<TParams, TResult> requestType,
Func<TParams, RequestContext<TResult>, Task> requestHandler,
bool overrideExisting = false);
/// <summary>
/// Sets the function to run when a request message of a specific
/// <paramref name="requestType"/> is received
/// </summary>
/// <param name="requestType">Configuration of the request message <paramref name="requestHandler"/> will handle</param>
/// <param name="requestHandler">What to do when a request message of <paramref name="requestType"/> is received</param>
/// <param name="overrideExisting">If <c>true</c>, any existing handler will be replaced with this one</param>
/// <typeparam name="TParams">Type of the parameters for the request, defined by <paramref name="requestType"/></typeparam>
/// <typeparam name="TResult">Type of the response to the request, defined by <paramref name="requestType"/></typeparam>
void SetRequestHandler<TParams, TResult>(
RequestType<TParams, TResult> requestType,
Action<TParams, RequestContext<TResult>> requestHandler,
bool overrideExisting = false);
/// <summary>
/// Sets the function to run when an event message of a specific configurat
/// <paramref name="eventType"/> is received
/// </summary>
/// <param name="eventType">Configuration of the event message <paramref name="eventHandler"/> will handle</param>
/// <param name="eventHandler">What to do when an event message of <paramref name="eventType"/> is received</param>
/// <param name="overrideExisting">If <c>true</c>, any existing handler will be replaced with this one</param>
/// <typeparam name="TParams">Type of the parameters for the event</typeparam>
void SetAsyncEventHandler<TParams>(
EventType<TParams> eventType,
Func<TParams, EventContext, Task> eventHandler,
bool overrideExisting = false);
/// <summary>
/// Sets the function to run when an event message of a specific
/// <paramref name="eventType"/> is received
/// </summary>
/// <param name="eventType">Configuration of the event message <paramref name="eventHandler"/> will handle</param>
/// <param name="eventHandler">What to do when an event message of <paramref name="eventType"/> is received</param>
/// <param name="overrideExisting">If <c>true</c>, any existing handler will be replaced with this one</param>
/// <typeparam name="TParams">Type of the parameters for the event</typeparam>
void SetEventHandler<TParams>(
EventType<TParams> eventType,
Action<TParams, EventContext> eventHandler,
bool overrideExisting = false);
}
}

View File

@@ -0,0 +1,27 @@
//
// 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;
using Microsoft.SqlTools.Hosting.Contracts;
namespace Microsoft.SqlTools.Hosting.Protocol
{
/// <summary>
/// Interface for objects that can requests via the JSON RPC channel
/// </summary>
public interface IRequestSender
{
/// <summary>
/// Sends a request over the JSON RPC channel. It will wait for a response to the message
/// before completing.
/// </summary>
/// <param name="requestType">Configuration of the request to send</param>
/// <param name="requestParams">Parameters for the request to send</param>
/// <typeparam name="TParams">Type of the parameters for the request, defined by <paramref name="requestType"/></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <returns></returns>
Task<TResult> SendRequest<TParams, TResult>(RequestType<TParams, TResult> requestType, TParams requestParams);
}
}

View File

@@ -0,0 +1,448 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Channels;
using Microsoft.SqlTools.Hosting.Contracts;
using Microsoft.SqlTools.Hosting.Contracts.Internal;
using Microsoft.SqlTools.Hosting.Utility;
using Microsoft.SqlTools.Hosting.v2;
namespace Microsoft.SqlTools.Hosting.Protocol
{
public class JsonRpcHost : IJsonRpcHost
{
#region Private Fields
internal readonly CancellationTokenSource cancellationTokenSource;
private readonly CancellationToken consumeInputCancellationToken;
private readonly CancellationToken consumeOutputCancellationToken;
internal readonly BlockingCollection<Message> outputQueue;
internal readonly Dictionary<string, Func<Message, Task>> eventHandlers;
internal readonly Dictionary<string, Func<Message, Task>> requestHandlers;
internal readonly ConcurrentDictionary<string, TaskCompletionSource<Message>> pendingRequests;
internal readonly ChannelBase protocolChannel;
internal Task consumeInputTask;
internal Task consumeOutputTask;
private bool isStarted;
#endregion
public JsonRpcHost(ChannelBase channel)
{
Validate.IsNotNull(nameof(channel), channel);
cancellationTokenSource = new CancellationTokenSource();
consumeInputCancellationToken = cancellationTokenSource.Token;
consumeOutputCancellationToken = cancellationTokenSource.Token;
outputQueue = new BlockingCollection<Message>(new ConcurrentQueue<Message>());
protocolChannel = channel;
eventHandlers = new Dictionary<string, Func<Message, Task>>();
requestHandlers = new Dictionary<string, Func<Message, Task>>();
pendingRequests = new ConcurrentDictionary<string, TaskCompletionSource<Message>>();
}
#region Start/Stop Methods
/// <summary>
/// Starts the JSON RPC host using the protocol channel that was provided
/// </summary>
public void Start()
{
// If we've already started, we can't start up again
if (isStarted)
{
throw new InvalidOperationException(SR.HostingJsonRpcHostAlreadyStarted);
}
// Make sure no other calls try to start the endpoint during startup
isStarted = true;
// Initialize the protocol channel
protocolChannel.Start();
protocolChannel.WaitForConnection().Wait();
// Start the input and output consumption threads
consumeInputTask = ConsumeInput();
consumeOutputTask = ConsumeOutput();
}
/// <summary>
/// Stops the JSON RPC host and the underlying protocol channel
/// </summary>
public void Stop()
{
// If we haven't started, we can't stop
if (!isStarted)
{
throw new InvalidOperationException(SR.HostingJsonRpcHostNotStarted);
}
// Make sure no future calls try to stop the endpoint during shutdown
isStarted = false;
// Shutdown the host
cancellationTokenSource.Cancel();
protocolChannel.Stop();
}
/// <summary>
/// Waits for input and output threads to naturally exit
/// </summary>
/// <exception cref="InvalidOperationException">Thrown if the host has not started</exception>
public void WaitForExit()
{
// If we haven't started everything, we can't wait for exit
if (!isStarted)
{
throw new InvalidOperationException(SR.HostingJsonRpcHostNotStarted);
}
// Join the input and output threads to this thread
Task.WaitAll(consumeInputTask, consumeOutputTask);
}
#endregion
#region Public Methods
/// <summary>
/// Sends an event, independent of any request
/// </summary>
/// <typeparam name="TParams">Event parameter type</typeparam>
/// <param name="eventType">Type of event being sent</param>
/// <param name="eventParams">Event parameters being sent</param>
/// <returns>Task that tracks completion of the send operation.</returns>
public void SendEvent<TParams>(
EventType<TParams> eventType,
TParams eventParams)
{
if (!protocolChannel.IsConnected)
{
throw new InvalidOperationException("SendEvent called when ProtocolChannel was not yet connected");
}
// Create a message from the event provided
Message message = Message.CreateEvent(eventType, eventParams);
outputQueue.Add(message);
}
/// <summary>
/// Sends a request, independent of any request
/// </summary>
/// <param name="requestType">Configuration of the request that is being sent</param>
/// <param name="requestParams">Contents of the request</param>
/// <typeparam name="TParams">Type of the message contents</typeparam>
/// <typeparam name="TResult">Type of the contents of the expected result of the request</typeparam>
/// <returns>Task that is completed when the </returns>
/// TODO: This doesn't properly handle error responses scenarios.
public async Task<TResult> SendRequest<TParams, TResult>(
RequestType<TParams, TResult> requestType,
TParams requestParams)
{
if (!protocolChannel.IsConnected)
{
throw new InvalidOperationException("SendRequest called when ProtocolChannel was not yet connected");
}
// Add a task completion source for the request's response
string messageId = Guid.NewGuid().ToString();
TaskCompletionSource<Message> responseTask = new TaskCompletionSource<Message>();
pendingRequests.TryAdd(messageId, responseTask);
// Send the request
outputQueue.Add(Message.CreateRequest(requestType, messageId, requestParams));
// Wait for the response
Message responseMessage = await responseTask.Task;
return responseMessage.GetTypedContents<TResult>();
}
/// <summary>
/// Sets the handler for an event with a given configuration
/// </summary>
/// <param name="eventType">Configuration of the event</param>
/// <param name="eventHandler">Function for handling the event</param>
/// <param name="overrideExisting">Whether or not to override any existing event handler for this method</param>
/// <typeparam name="TParams">Type of the parameters for the event</typeparam>
public void SetAsyncEventHandler<TParams>(
EventType<TParams> eventType,
Func<TParams, EventContext, Task> eventHandler,
bool overrideExisting = false)
{
Validate.IsNotNull(nameof(eventType), eventType);
Validate.IsNotNull(nameof(eventHandler), eventHandler);
if (overrideExisting)
{
// Remove the existing handler so a new one can be set
eventHandlers.Remove(eventType.MethodName);
}
Func<Message, Task> handler = eventMessage =>
eventHandler(eventMessage.GetTypedContents<TParams>(), new EventContext(outputQueue));
eventHandlers.Add(eventType.MethodName, handler);
}
/// <summary>
/// Creates a Func based that wraps the action in a task and calls the Func-based overload
/// </summary>
/// <param name="eventType">Configuration of the event</param>
/// <param name="eventHandler">Function for handling the event</param>
/// <param name="overrideExisting">Whether or not to override any existing event handler for this method</param>
/// <typeparam name="TParams">Type of the parameters for the event</typeparam>
public void SetEventHandler<TParams>(
EventType<TParams> eventType,
Action<TParams, EventContext> eventHandler,
bool overrideExisting = false)
{
Validate.IsNotNull(nameof(eventHandler), eventHandler);
Func<TParams, EventContext, Task> eventFunc = (p, e) => Task.Run(() => eventHandler(p, e));
SetAsyncEventHandler(eventType, eventFunc, overrideExisting);
}
/// <summary>
/// Sets the handler for a request with a given configuration
/// </summary>
/// <param name="requestType">Configuration of the request</param>
/// <param name="requestHandler">Function for handling the request</param>
/// <param name="overrideExisting">Whether or not to override any existing request handler for this method</param>
/// <typeparam name="TParams">Type of the parameters for the request</typeparam>
/// <typeparam name="TResult">Type of the parameters for the response</typeparam>
public void SetAsyncRequestHandler<TParams, TResult>(
RequestType<TParams, TResult> requestType,
Func<TParams, RequestContext<TResult>, Task> requestHandler,
bool overrideExisting = false)
{
Validate.IsNotNull(nameof(requestType), requestType);
Validate.IsNotNull(nameof(requestHandler), requestHandler);
if (overrideExisting)
{
// Remove the existing handler so a new one can be set
requestHandlers.Remove(requestType.MethodName);
}
// Setup the wrapper around the handler
Func<Message, Task> handler = requestMessage =>
requestHandler(requestMessage.GetTypedContents<TParams>(), new RequestContext<TResult>(requestMessage, outputQueue));
requestHandlers.Add(requestType.MethodName, handler);
}
/// <summary>
/// Creates a Func based that wraps the action in a task and calls the Func-based overload
/// </summary>
/// /// <param name="requestType">Configuration of the request</param>
/// <param name="requestHandler">Function for handling the request</param>
/// <param name="overrideExisting">Whether or not to override any existing request handler for this method</param>
/// <typeparam name="TParams">Type of the parameters for the request</typeparam>
/// <typeparam name="TResult">Type of the parameters for the response</typeparam>
public void SetRequestHandler<TParams, TResult>(
RequestType<TParams, TResult> requestType,
Action<TParams, RequestContext<TResult>> requestHandler,
bool overrideExisting = false)
{
Validate.IsNotNull(nameof(requestHandler), requestHandler);
Func<TParams, RequestContext<TResult>, Task> requestFunc = (p, e) => Task.Run(() => requestHandler(p, e));
SetAsyncRequestHandler(requestType, requestFunc, overrideExisting);
}
#endregion
#region Message Processing Tasks
internal Task ConsumeInput()
{
return Task.Factory.StartNew(async () =>
{
while (!consumeInputCancellationToken.IsCancellationRequested)
{
Message incomingMessage;
try
{
// Read message from the input channel
incomingMessage = await protocolChannel.MessageReader.ReadMessage();
}
catch (EndOfStreamException)
{
// The stream has ended, end the input message loop
break;
}
catch (Exception e)
{
// Log the error and send an error event to the client
string message = $"Exception occurred while receiving input message: {e.Message}";
Logger.Write(TraceEventType.Error, message);
// TODO: Add event to output queue, and unit test it
// Continue the loop
continue;
}
// Verbose logging
string logMessage =
$"Received message with Id[{incomingMessage.Id}] of type[{incomingMessage.MessageType}] and method[{incomingMessage.Method}]";
Logger.Write(TraceEventType.Verbose, logMessage);
// Process the message
try
{
await DispatchMessage(incomingMessage);
}
catch (MethodHandlerDoesNotExistException)
{
// Method could not be handled, if the message was a request, send an error back to the client
// TODO: Localize
string mnfLogMessage =
$"Failed to find method handler for type[{incomingMessage.MessageType}] and method[{incomingMessage.Method}]";
Logger.Write(TraceEventType.Warning, mnfLogMessage);
if (incomingMessage.MessageType == MessageType.Request)
{
// TODO: Localize
Error mnfError = new Error {Code = -32601, Message = "Method not found"};
Message errorMessage = Message.CreateResponseError(incomingMessage.Id, mnfError);
outputQueue.Add(errorMessage, consumeInputCancellationToken);
}
}
catch (Exception e)
{
// General errors should be logged but not halt the processing loop
string geLogMessage =
$"Exception thrown when handling message of type[{incomingMessage.MessageType}] and method[{incomingMessage.Method}]: {e}";
Logger.Write(TraceEventType.Error, geLogMessage);
// TODO: Should we be returning a response for failing requests?
}
}
Logger.Write(TraceEventType.Warning, "Exiting consume input loop!");
}, consumeOutputCancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
}
internal Task ConsumeOutput()
{
return Task.Factory.StartNew(async () =>
{
while (!consumeOutputCancellationToken.IsCancellationRequested)
{
Message outgoingMessage;
try
{
// Read message from the output queue
outgoingMessage = outputQueue.Take(consumeOutputCancellationToken);
}
catch (OperationCanceledException)
{
// Cancelled during taking, end the loop
break;
}
catch (Exception e)
{
// If we hit an exception here, it is unrecoverable
string message = string.Format("Unexpected occurred while receiving output message: {0}", e.Message);
Logger.Write(TraceEventType.Error, message);
break;
}
// Send the message
string logMessage = string.Format("Sending message of type[{0}] and method[{1}]",
outgoingMessage.MessageType, outgoingMessage.Method);
Logger.Write(TraceEventType.Verbose, logMessage);
await protocolChannel.MessageWriter.WriteMessage(outgoingMessage);
}
Logger.Write(TraceEventType.Warning, "Exiting consume output loop!");
}, consumeOutputCancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
}
internal async Task DispatchMessage(Message messageToDispatch)
{
Task handlerToAwait = null;
switch (messageToDispatch.MessageType)
{
case MessageType.Request:
Func<Message, Task> requestHandler;
if (requestHandlers.TryGetValue(messageToDispatch.Method, out requestHandler))
{
handlerToAwait = requestHandler(messageToDispatch);
}
else
{
throw new MethodHandlerDoesNotExistException(MessageType.Request, messageToDispatch.Method);
}
break;
case MessageType.Response:
TaskCompletionSource<Message> requestTask;
if (pendingRequests.TryRemove(messageToDispatch.Id, out requestTask))
{
requestTask.SetResult(messageToDispatch);
return;
}
else
{
throw new MethodHandlerDoesNotExistException(MessageType.Response, "response");
}
case MessageType.Event:
Func<Message, Task> eventHandler;
if (eventHandlers.TryGetValue(messageToDispatch.Method, out eventHandler))
{
handlerToAwait = eventHandler(messageToDispatch);
}
else
{
throw new MethodHandlerDoesNotExistException(MessageType.Event, messageToDispatch.Method);
}
break;
default:
// TODO: This case isn't handled properly
break;
}
// Skip processing if there isn't anything to do
if (handlerToAwait == null)
{
return;
}
// Run the handler
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;
}
}
}
#endregion
}
}

View File

@@ -0,0 +1,252 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Diagnostics;
using Microsoft.SqlTools.Hosting.Contracts;
using Microsoft.SqlTools.Hosting.Contracts.Internal;
using Microsoft.SqlTools.Hosting.v2;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;
namespace Microsoft.SqlTools.Hosting.Protocol
{
/// <summary>
/// Defines all possible message types.
/// </summary>
public enum MessageType
{
Request,
Response,
ResponseError,
Event
}
/// <summary>
/// Representation for a JSON RPC message. Provides logic for converting back and forth from
/// string
/// </summary>
[DebuggerDisplay("MessageType = {MessageType.ToString()}, Method = {Method}, Id = {Id}")]
public class Message
{
#region Constants
private const string JsonRpcVersion = "2.0";
private static readonly JsonSerializerSettings JsonSerializerSettings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
private static readonly JsonSerializer ContentSerializer = JsonSerializer.Create(JsonSerializerSettings);
#endregion
#region Construction
private Message(MessageType messageType, JToken contents)
{
MessageType = messageType;
Contents = contents;
}
/// <summary>
/// Creates a message with a Request type
/// </summary>
/// <param name="requestType">Configuration for the request</param>
/// <param name="id">ID of the message</param>
/// <param name="contents">Contents of the message</param>
/// <typeparam name="TParams">Type of the contents of the message</typeparam>
/// <typeparam name="TResult">Type of the contents of the results to this request</typeparam>
/// <returns>Message with a Request type</returns>
public static Message CreateRequest<TParams, TResult>(
RequestType<TParams, TResult> requestType,
string id,
TParams contents)
{
JToken contentsToken = contents == null ? null : JToken.FromObject(contents, ContentSerializer);
return new Message(MessageType.Request, contentsToken)
{
Id = id,
Method = requestType.MethodName
};
}
/// <summary>
/// Creates a message with a Response type.
/// </summary>
/// <param name="id">The sequence ID 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 CreateResponse<TParams>(
string id,
TParams contents)
{
JToken contentsToken = contents == null ? null : JToken.FromObject(contents, ContentSerializer);
return new Message(MessageType.Response, contentsToken)
{
Id = id
};
}
/// <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="error">The error details of the response.</param>
/// <returns>A message with a Response type and error details.</returns>
public static Message CreateResponseError(
string id,
Error error)
{
JToken errorToken = error == null ? null : JToken.FromObject(error, ContentSerializer);
return new Message(MessageType.ResponseError, errorToken)
{
Id = id
};
}
/// <summary>
/// Creates a message with an Event type.
/// </summary>
/// <param name="eventType">Configuration for the event message</param>
/// <param name="contents">The contents of the event.</param>
/// <typeparam name="TParams"></typeparam>
/// <returns>A message with an Event type</returns>
public static Message CreateEvent<TParams>(
EventType<TParams> eventType,
TParams contents)
{
JToken contentsToken = contents == null ? null : JToken.FromObject(contents, ContentSerializer);
return new Message(MessageType.Event, contentsToken)
{
Method = eventType.MethodName
};
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the message type.
/// </summary>
public MessageType MessageType { get; }
/// <summary>
/// Gets or sets the message's sequence ID.
/// </summary>
public string Id { get; private set; }
/// <summary>
/// Gets or sets the message's method/command name.
/// </summary>
public string Method { get; private set; }
/// <summary>
/// Gets or sets a JToken containing the contents of the message.
/// </summary>
public JToken Contents { get; }
#endregion
#region Serialization/Deserialization
public static Message Deserialize(string jsonString)
{
// Deserialize the object from the JSON into an intermediate object
JObject messageObject = JObject.Parse(jsonString);
JToken token;
// Ensure there's a JSON RPC version or else it's invalid
if (!messageObject.TryGetValue("jsonrpc", out token) || token.Value<string>() != JsonRpcVersion)
{
throw new MessageParseException(null, SR.HostingJsonRpcVersionMissing);
}
if (messageObject.TryGetValue("id", out token))
{
// Message with ID is a Request or Response
string messageId = token.ToString();
if (messageObject.TryGetValue("result", out token))
{
return new Message(MessageType.Response, token) {Id = messageId};
}
if (messageObject.TryGetValue("error", out token))
{
return new Message(MessageType.ResponseError, token) {Id = messageId};
}
// Message without result/error is a Request
JToken messageParams;
messageObject.TryGetValue("params", out messageParams);
if (!messageObject.TryGetValue("method", out token))
{
throw new MessageParseException(null, SR.HostingMessageMissingMethod);
}
return new Message(MessageType.Request, messageParams) {Id = messageId, Method = token.ToString()};
}
else
{
// Messages without an id are events
JToken messageParams;
messageObject.TryGetValue("params", out messageParams);
if (!messageObject.TryGetValue("method", out token))
{
throw new MessageParseException(null, SR.HostingMessageMissingMethod);
}
return new Message(MessageType.Event, messageParams) {Method = token.ToString()};
}
}
public string Serialize()
{
JObject messageObject = new JObject
{
{"jsonrpc", JToken.FromObject(JsonRpcVersion)}
};
switch (MessageType)
{
case MessageType.Request:
messageObject.Add("id", JToken.FromObject(Id));
messageObject.Add("method", Method);
messageObject.Add("params", Contents);
break;
case MessageType.Event:
messageObject.Add("method", Method);
messageObject.Add("params", Contents);
break;
case MessageType.Response:
messageObject.Add("id", JToken.FromObject(Id));
messageObject.Add("result", Contents);
break;
case MessageType.ResponseError:
messageObject.Add("id", JToken.FromObject(Id));
messageObject.Add("error", Contents);
break;
}
return JsonConvert.SerializeObject(messageObject);
}
public TContents GetTypedContents<TContents>()
{
TContents typedContents = default(TContents);
if (Contents != null)
{
typedContents = Contents.ToObject<TContents>();
}
return typedContents;
}
#endregion
}
}

View File

@@ -0,0 +1,258 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Utility;
using Microsoft.SqlTools.Hosting.v2;
namespace Microsoft.SqlTools.Hosting.Protocol
{
public class MessageReader
{
#region Private Fields
public const int DefaultBufferSize = 8192;
private const double BufferResizeTrigger = 0.25;
private const int CR = 0x0D;
private const int LF = 0x0A;
private static readonly string[] NewLineDelimiters = { Environment.NewLine };
private readonly Stream inputStream;
private bool needsMoreData = true;
private int readOffset;
private int bufferEndOffset;
private byte[] messageBuffer;
private int expectedContentLength;
private Dictionary<string, string> messageHeaders;
internal enum ReadState
{
Headers,
Content
}
#endregion
#region Constructors
public MessageReader(Stream inputStream, Encoding messageEncoding = null)
{
Validate.IsNotNull("streamReader", inputStream);
this.inputStream = inputStream;
MessageEncoding = messageEncoding ?? Encoding.UTF8;
messageBuffer = new byte[DefaultBufferSize];
}
#endregion
#region Testable Properties
internal byte[] MessageBuffer
{
get => messageBuffer;
set => messageBuffer = value;
}
internal ReadState CurrentState { get; private set; }
internal Encoding MessageEncoding { get; private set; }
#endregion
#region Public Methods
public virtual async Task<Message> ReadMessage()
{
string messageContent = null;
// Do we need to read more data or can we process the existing buffer?
while (!needsMoreData || await ReadNextChunk())
{
// Clear the flag since we should have what we need now
needsMoreData = false;
// Do we need to look for message headers?
if (CurrentState == ReadState.Headers && !TryReadMessageHeaders())
{
// If we don't have enough data to read headers yet, keep reading
needsMoreData = true;
continue;
}
// Do we need to look for message content?
if (CurrentState == ReadState.Content && !TryReadMessageContent(out messageContent))
{
// If we don't have enough data yet to construct the content, keep reading
needsMoreData = true;
continue;
}
// We've read a message now, break out of the loop
break;
}
// Now that we have a message, reset the buffer's state
ShiftBufferBytesAndShrink(readOffset);
// Return the parsed message
return Message.Deserialize(messageContent);
}
#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)(messageBuffer.Length - bufferEndOffset) / messageBuffer.Length < BufferResizeTrigger)
{
// Double the size of the buffer
Array.Resize(ref messageBuffer, messageBuffer.Length * 2);
}
// Read the next chunk into the message buffer
int readLength =
await inputStream.ReadAsync(messageBuffer, bufferEndOffset, messageBuffer.Length - bufferEndOffset);
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.
throw new EndOfStreamException(SR.HostingUnexpectedEndOfStream);
}
return true;
}
private bool TryReadMessageHeaders()
{
int scanOffset = readOffset;
// Scan for the final double-newline that marks the end of the header lines
while (scanOffset + 3 < bufferEndOffset &&
(messageBuffer[scanOffset] != CR ||
messageBuffer[scanOffset + 1] != LF ||
messageBuffer[scanOffset + 2] != CR ||
messageBuffer[scanOffset + 3] != LF))
{
scanOffset++;
}
// Make sure we haven't reached the end of the buffer without finding a separator (e.g CRLFCRLF)
if (scanOffset + 3 >= bufferEndOffset)
{
return false;
}
// Convert the header block into a array of lines
var headers = Encoding.ASCII.GetString(messageBuffer, readOffset, scanOffset)
.Split(NewLineDelimiters, StringSplitOptions.RemoveEmptyEntries);
try
{
// Read each header and store it in the dictionary
messageHeaders = new Dictionary<string, string>();
foreach (var header in headers)
{
int currentLength = header.IndexOf(':');
if (currentLength == -1)
{
throw new ArgumentException(SR.HostingHeaderMissingColon);
}
var key = header.Substring(0, currentLength);
var value = header.Substring(currentLength + 1).Trim();
messageHeaders[key] = value;
}
// Parse out the content length as an int
string contentLengthString;
if (!messageHeaders.TryGetValue("Content-Length", out contentLengthString))
{
throw new MessageParseException("", SR.HostingHeaderMissingContentLengthHeader);
}
// Parse the content length to an integer
if (!int.TryParse(contentLengthString, out expectedContentLength))
{
throw new MessageParseException("", SR.HostingHeaderMissingContentLengthValue);
}
}
catch (Exception)
{
// The content length was invalid or missing. Trash the buffer we've read
ShiftBufferBytesAndShrink(scanOffset + 4);
throw;
}
// Skip past the headers plus the newline characters
readOffset += scanOffset + 4;
// Done reading headers, now read content
CurrentState = ReadState.Content;
return true;
}
private bool TryReadMessageContent(out string messageContent)
{
messageContent = null;
// Do we have enough bytes to reach the expected length?
if (bufferEndOffset - readOffset < expectedContentLength)
{
return false;
}
// Convert the message contents to a string using the specified encoding
messageContent = MessageEncoding.GetString(messageBuffer, readOffset, expectedContentLength);
readOffset += expectedContentLength;
// Done reading content, now look for headers for the next message
CurrentState = ReadState.Headers;
return true;
}
private void ShiftBufferBytesAndShrink(int bytesToRemove)
{
// Create a new buffer that is shrunken by the number of bytes to remove
// Note: by using Max, we can guarantee a buffer of at least default buffer size
byte[] newBuffer = new byte[Math.Max(messageBuffer.Length - bytesToRemove, DefaultBufferSize)];
// If we need to do shifting, do the shifting
if (bytesToRemove <= messageBuffer.Length)
{
// Copy the existing buffer starting at the offset to remove
Buffer.BlockCopy(messageBuffer, bytesToRemove, newBuffer, 0, bufferEndOffset - bytesToRemove);
}
// Make the new buffer the message buffer
messageBuffer = newBuffer;
// Reset the read offset and the end offset
readOffset = 0;
bufferEndOffset -= bytesToRemove;
}
#endregion
}
}

View File

@@ -0,0 +1,60 @@
//
// 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 System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Utility;
namespace Microsoft.SqlTools.Hosting.Protocol
{
public class MessageWriter
{
private const string ContentLength = "Content-Length: ";
private const string ContentType = "Content-Type: application/json";
private const string HeaderSeparator = "\r\n";
private const string HeaderEnd = "\r\n\r\n";
private readonly Stream outputStream;
private readonly AsyncLock writeLock = new AsyncLock();
public MessageWriter(Stream outputStream)
{
Validate.IsNotNull("streamWriter", outputStream);
this.outputStream = outputStream;
}
public virtual async Task WriteMessage(Message messageToWrite)
{
Validate.IsNotNull("messageToWrite", messageToWrite);
// Log the JSON representation of the message
string logMessage = string.Format("Sending message of type[{0}] and method[{1}]",
messageToWrite.MessageType, messageToWrite.Method);
Logger.Write(TraceEventType.Verbose, logMessage);
string serializedMessage = messageToWrite.Serialize();
// TODO: Allow encoding to be passed in
byte[] messageBytes = Encoding.UTF8.GetBytes(serializedMessage);
string headers = ContentLength + messageBytes.Length + HeaderSeparator
+ ContentType + HeaderEnd;
byte[] headerBytes = Encoding.ASCII.GetBytes(headers);
// 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 writeLock.LockAsync())
{
// Send the message
await outputStream.WriteAsync(headerBytes, 0, headerBytes.Length);
await outputStream.WriteAsync(messageBytes, 0, messageBytes.Length);
await outputStream.FlushAsync();
}
}
}
}

View File

@@ -0,0 +1,57 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Collections.Concurrent;
using Microsoft.SqlTools.Hosting.Contracts;
using Microsoft.SqlTools.Hosting.Contracts.Internal;
namespace Microsoft.SqlTools.Hosting.Protocol
{
public class RequestContext<TResult> : IEventSender
{
internal readonly BlockingCollection<Message> messageQueue;
internal readonly Message requestMessage;
public RequestContext(Message message, BlockingCollection<Message> outgoingMessageQueue)
{
// TODO: Either 1) make this constructor internal and provide a tes framework for validating
// or 2) extract an interface for requestcontext to allow users to mock
requestMessage = message;
messageQueue = outgoingMessageQueue;
}
public virtual void SendResult(TResult resultDetails)
{
Message message = Message.CreateResponse(requestMessage.Id, resultDetails);
messageQueue.Add(message);
}
public virtual void SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams)
{
Message message = Message.CreateEvent(eventType, eventParams);
messageQueue.Add(message);
}
public virtual void SendError(string errorMessage, int errorCode = 0)
{
// Build the error message
Error error = new Error
{
Message = errorMessage,
Code = errorCode
};
Message message = Message.CreateResponseError(requestMessage.Id, error);
messageQueue.Add(message);
}
public virtual void SendError(Exception e)
{
// Overload to use the parameterized error handler
SendError(e.Message, e.HResult);
}
}
}