mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-02-16 10:58:30 -05:00
Add v2 of the Hosting Service and build nuget packages for it (#675)
* Port v2 of Hosting service to SqlToolsService - Renamed project to .v2 so that existing hosted service isn't impacted - Copied over the CoreServices project which contains ConnectionServiceCore and other reusable services for anything interacting with MSSQL - Ported unit test project across and verified tests run. * Nuget package support for reusable DLLs * Use 1.1 version per Karl's suggestion * Use correct license URL and project URL * Use new SMO packages
This commit is contained in:
58
src/Microsoft.SqlTools.Hosting.v2/Channels/ChannelBase.cs
Normal file
58
src/Microsoft.SqlTools.Hosting.v2/Channels/ChannelBase.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// 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.Protocol;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Channels
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a base implementation for servers and their clients over a
|
||||
/// single kind of communication channel.
|
||||
/// </summary>
|
||||
public abstract class ChannelBase
|
||||
{
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean that is true if the channel is connected or false if not.
|
||||
/// </summary>
|
||||
public bool IsConnected { get; protected internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MessageReader for reading messages from the channel.
|
||||
/// </summary>
|
||||
public MessageReader MessageReader { get; protected internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the MessageWriter for writing messages to the channel.
|
||||
/// </summary>
|
||||
public MessageWriter MessageWriter { get; protected internal set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region Abstract Methods
|
||||
|
||||
/// <summary>
|
||||
/// Starts the channel and initializes the MessageDispatcher.
|
||||
/// </summary>
|
||||
public abstract void Start();
|
||||
|
||||
/// <summary>
|
||||
/// Stops the channel.
|
||||
/// </summary>
|
||||
public abstract void Stop();
|
||||
|
||||
/// <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();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
111
src/Microsoft.SqlTools.Hosting.v2/Channels/StdioClientChannel.cs
Normal file
111
src/Microsoft.SqlTools.Hosting.v2/Channels/StdioClientChannel.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// 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.Protocol;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Channels
|
||||
{
|
||||
/// <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 readonly string serviceProcessPath;
|
||||
private readonly string serviceProcessArguments;
|
||||
|
||||
private Stream inputStream;
|
||||
private Stream outputStream;
|
||||
private Process serviceProcess;
|
||||
|
||||
/// <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)
|
||||
{
|
||||
serviceProcessPath = serverProcessPath;
|
||||
|
||||
if (serverProcessArguments != null)
|
||||
{
|
||||
serviceProcessArguments = string.Join(" ", serverProcessArguments);
|
||||
}
|
||||
}
|
||||
|
||||
public int ProcessId { get; private set; }
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
serviceProcess = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = serviceProcessPath,
|
||||
Arguments = serviceProcessArguments,
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
StandardOutputEncoding = Encoding.UTF8
|
||||
},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
// Start the process
|
||||
serviceProcess.Start();
|
||||
ProcessId = serviceProcess.Id;
|
||||
|
||||
// Open the standard input/output streams
|
||||
inputStream = serviceProcess.StandardOutput.BaseStream;
|
||||
outputStream = serviceProcess.StandardInput.BaseStream;
|
||||
|
||||
// Set up the message reader and writer
|
||||
MessageReader = new MessageReader(inputStream);
|
||||
MessageWriter = new MessageWriter(outputStream);
|
||||
|
||||
IsConnected = true;
|
||||
}
|
||||
|
||||
public override Task WaitForConnection()
|
||||
{
|
||||
// We're always connected immediately in the stdio channel
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public override void Stop()
|
||||
{
|
||||
if (inputStream != null)
|
||||
{
|
||||
inputStream.Dispose();
|
||||
inputStream = null;
|
||||
}
|
||||
|
||||
if (outputStream != null)
|
||||
{
|
||||
outputStream.Dispose();
|
||||
outputStream = null;
|
||||
}
|
||||
|
||||
if (MessageReader != null)
|
||||
{
|
||||
MessageReader = null;
|
||||
}
|
||||
|
||||
if (MessageWriter != null)
|
||||
{
|
||||
MessageWriter = null;
|
||||
}
|
||||
|
||||
serviceProcess.Kill();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.Hosting.Protocol;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Channels
|
||||
{
|
||||
/// <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;
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
#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
|
||||
inputStream = System.Console.OpenStandardInput();
|
||||
outputStream = System.Console.OpenStandardOutput();
|
||||
|
||||
// Set up the reader and writer
|
||||
MessageReader = new MessageReader(inputStream);
|
||||
MessageWriter = new MessageWriter(outputStream);
|
||||
|
||||
IsConnected = true;
|
||||
}
|
||||
|
||||
public override void Stop()
|
||||
{
|
||||
// No default implementation needed, streams will be
|
||||
// disposed on process shutdown.
|
||||
}
|
||||
|
||||
public override Task WaitForConnection()
|
||||
{
|
||||
// We're always connected immediately in the stdio channel
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// 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.Composition;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Extensibility
|
||||
{
|
||||
/// <summary>
|
||||
/// Base attribute class for all export definitions.
|
||||
/// </summary>
|
||||
[MetadataAttribute]
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public abstract class ExportStandardMetadataAttribute : ExportAttribute, IStandardMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Base class for DAC extensibility exports
|
||||
/// </summary>
|
||||
protected ExportStandardMetadataAttribute(Type contractType, string id, string displayName = null)
|
||||
: base(contractType)
|
||||
{
|
||||
Id = id;
|
||||
DisplayName = displayName;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The version of this extension
|
||||
/// </summary>
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The id of the extension
|
||||
/// </summary>
|
||||
public string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The display name for the extension
|
||||
/// </summary>
|
||||
public virtual string DisplayName { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
//
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Composition.Convention;
|
||||
using System.Composition.Hosting;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Loader;
|
||||
using Microsoft.SqlTools.Hosting.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Extensibility
|
||||
{
|
||||
/// <summary>
|
||||
/// A MEF-based service provider. Supports any MEF-based configuration but is optimized for
|
||||
/// service discovery over a set of DLLs in an application scope. Any service registering using
|
||||
/// the <c>[Export(IServiceContract)]</c> attribute will be discovered and used by this service
|
||||
/// provider if it's in the set of Assemblies / Types specified during its construction. Manual
|
||||
/// override of this is supported by calling
|
||||
/// <see cref="RegisteredServiceProvider.RegisterSingleService" /> and similar methods, since
|
||||
/// this will initialize that service contract and avoid the MEF-based search and discovery
|
||||
/// process. This allows the service provider to link into existing singleton / known services
|
||||
/// while using MEF-based dependency injection and inversion of control for most of the code.
|
||||
/// </summary>
|
||||
public class ExtensionServiceProvider : RegisteredServiceProvider
|
||||
{
|
||||
private readonly Func<ConventionBuilder, ContainerConfiguration> config;
|
||||
|
||||
public ExtensionServiceProvider(Func<ConventionBuilder, ContainerConfiguration> config)
|
||||
{
|
||||
Validate.IsNotNull(nameof(config), config);
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a service provider by loading a set of named assemblies, expected to be <paramref name="directory"/>
|
||||
/// </summary>
|
||||
/// <param name="directory">Directory to search for included assemblies</param>
|
||||
/// <param name="inclusionList">full DLL names, case insensitive, of assemblies to include</param>
|
||||
/// <returns><see cref="ExtensionServiceProvider"/> instance</returns>
|
||||
public static ExtensionServiceProvider CreateFromAssembliesInDirectory(string directory, IList<string> inclusionList)
|
||||
{
|
||||
//AssemblyLoadContext context = new AssemblyLoader(directory);
|
||||
var assemblyPaths = Directory.GetFiles(directory, "*.dll", SearchOption.TopDirectoryOnly);
|
||||
|
||||
List<Assembly> assemblies = new List<Assembly>();
|
||||
foreach (var path in assemblyPaths)
|
||||
{
|
||||
// skip DLL files not in inclusion list
|
||||
bool isInList = false;
|
||||
foreach (var item in inclusionList)
|
||||
{
|
||||
if (path.EndsWith(item, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
isInList = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isInList)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
assemblies.Add(AssemblyLoadContext.Default.LoadFromAssemblyPath(path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// we expect exceptions trying to scan all DLLs since directory contains native libraries
|
||||
}
|
||||
}
|
||||
|
||||
return Create(assemblies);
|
||||
}
|
||||
|
||||
public static ExtensionServiceProvider Create(IEnumerable<Assembly> assemblies)
|
||||
{
|
||||
Validate.IsNotNull(nameof(assemblies), assemblies);
|
||||
return new ExtensionServiceProvider(conventions => new ContainerConfiguration().WithAssemblies(assemblies, conventions));
|
||||
}
|
||||
|
||||
public static ExtensionServiceProvider Create(IEnumerable<Type> types)
|
||||
{
|
||||
Validate.IsNotNull(nameof(types), types);
|
||||
return new ExtensionServiceProvider(conventions => new ContainerConfiguration().WithParts(types, conventions));
|
||||
}
|
||||
|
||||
protected override IEnumerable<T> GetServicesImpl<T>()
|
||||
{
|
||||
EnsureExtensionStoreRegistered<T>();
|
||||
return base.GetServicesImpl<T>();
|
||||
}
|
||||
|
||||
private void EnsureExtensionStoreRegistered<T>()
|
||||
{
|
||||
if (!services.ContainsKey(typeof(T)))
|
||||
{
|
||||
ExtensionStore store = new ExtensionStore(typeof(T), config);
|
||||
Register(() => store.GetExports<T>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A store for MEF exports of a specific type. Provides basic wrapper functionality around MEF to standarize how
|
||||
/// we lookup types and return to callers.
|
||||
/// </summary>
|
||||
public class ExtensionStore
|
||||
{
|
||||
private readonly CompositionHost host;
|
||||
private IList exports;
|
||||
private readonly Type contractType;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the store with a type to lookup exports of, and a function that configures the
|
||||
/// lookup parameters.
|
||||
/// </summary>
|
||||
/// <param name="contractType">Type to use as a base for all extensions being looked up</param>
|
||||
/// <param name="configure">Function that returns the configuration to be used</param>
|
||||
public ExtensionStore(Type contractType, Func<ConventionBuilder, ContainerConfiguration> configure)
|
||||
{
|
||||
Validate.IsNotNull(nameof(contractType), contractType);
|
||||
Validate.IsNotNull(nameof(configure), configure);
|
||||
this.contractType = contractType;
|
||||
ConventionBuilder builder = GetExportBuilder();
|
||||
ContainerConfiguration config = configure(builder);
|
||||
host = config.CreateContainer();
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetExports<T>()
|
||||
{
|
||||
if (exports == null)
|
||||
{
|
||||
exports = host.GetExports(contractType).ToList();
|
||||
}
|
||||
return exports.Cast<T>();
|
||||
}
|
||||
|
||||
private ConventionBuilder GetExportBuilder()
|
||||
{
|
||||
// Define exports as matching a parent type, export as that parent type
|
||||
var builder = new ConventionBuilder();
|
||||
builder.ForTypesDerivedFrom(contractType).Export(exportConventionBuilder => exportConventionBuilder.AsContractType(contractType));
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// 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.Extensibility
|
||||
{
|
||||
/// <summary>
|
||||
/// A Service that expects to lookup other services. Using this interface on an exported service
|
||||
/// will ensure the <see cref="SetServiceProvider(IMultiServiceProvider)"/> method is called during
|
||||
/// service initialization
|
||||
/// </summary>
|
||||
public interface IComposableService
|
||||
{
|
||||
/// <summary>
|
||||
/// Supports settings the service provider being used to initialize the service.
|
||||
/// This is useful to look up other services and use them in your own service.
|
||||
/// </summary>
|
||||
void SetServiceProvider(IMultiServiceProvider provider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// 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.Protocol;
|
||||
using Microsoft.SqlTools.Hosting.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Extensibility
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a hosted service that communicates with external processes via
|
||||
/// messages passed over the <see cref="ServiceHost"/>. The service defines
|
||||
/// a standard initialization method where it can hook up to the host.
|
||||
/// </summary>
|
||||
public interface IHostedService
|
||||
{
|
||||
/// <summary>
|
||||
/// Callback to initialize this service
|
||||
/// </summary>
|
||||
/// <param name="serviceHost"><see cref="IServiceHost"/> which supports registering
|
||||
/// event handlers and other callbacks for messages passed to external callers</param>
|
||||
void InitializeService(IServiceHost serviceHost);
|
||||
|
||||
/// <summary>
|
||||
/// What is the service type that you wish to register?
|
||||
/// </summary>
|
||||
Type ServiceType { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for <see cref="IHostedService"/> implementations that handles defining the <see cref="ServiceType"/>
|
||||
/// being registered. This simplifies service registration. This also implements <see cref="IComposableService"/> which
|
||||
/// allows injection of the service provider for lookup of other services.
|
||||
///
|
||||
/// Extending classes should implement per below code example
|
||||
/// <code>
|
||||
/// [Export(typeof(IHostedService)]
|
||||
/// MyService : HostedService<MyService>
|
||||
/// {
|
||||
/// public override void InitializeService(IServiceHost serviceHost)
|
||||
/// {
|
||||
/// serviceHost.SetRequestHandler(MyRequest.Type, HandleMyRequest);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type to be registered for lookup in the service provider</typeparam>
|
||||
public abstract class HostedService<T> : IHostedService, IComposableService
|
||||
{
|
||||
protected IMultiServiceProvider ServiceProvider { get; private set; }
|
||||
|
||||
public virtual void SetServiceProvider(IMultiServiceProvider provider)
|
||||
{
|
||||
ServiceProvider = provider;
|
||||
}
|
||||
|
||||
public Type ServiceType => typeof(T);
|
||||
|
||||
protected async Task<THandler> HandleRequestAsync<THandler>(
|
||||
Func<Task<THandler>> handler,
|
||||
RequestContext<THandler> requestContext,
|
||||
string requestType)
|
||||
{
|
||||
Logger.Instance.Write(LogLevel.Verbose, requestType);
|
||||
|
||||
try
|
||||
{
|
||||
THandler result = await handler();
|
||||
requestContext.SendResult(result);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
requestContext.SendError(ex.ToString());
|
||||
}
|
||||
return default(THandler);
|
||||
}
|
||||
protected async Task<THandler> HandleSyncRequestAsAsync<THandler>(
|
||||
Func<THandler> handler,
|
||||
RequestContext<THandler> requestContext,
|
||||
string requestType)
|
||||
{
|
||||
Logger.Instance.Write(LogLevel.Verbose, requestType);
|
||||
return await Task.Factory.StartNew(() => {
|
||||
try
|
||||
{
|
||||
THandler result = handler();
|
||||
requestContext.SendResult(result);
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
requestContext.SendError(ex.ToString());
|
||||
}
|
||||
return default(THandler);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public abstract void InitializeService(IServiceHost serviceHost);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// 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.Linq;
|
||||
using Microsoft.SqlTools.Hosting.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Extensibility
|
||||
{
|
||||
public interface IMultiServiceProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a service of a specific type. It is expected that only 1 instance of this type will be
|
||||
/// available
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of service to be found</typeparam>
|
||||
/// <returns>Instance of T or null if not found</returns>
|
||||
/// <exception cref="InvalidOperationException">The input sequence contains more than one element.-or-The input sequence is empty.</exception>
|
||||
T GetService<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a service of a specific type. The first service matching the specified filter will be returned
|
||||
/// available
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of service to be found</typeparam>
|
||||
/// <param name="filter">Filter to use in </param>
|
||||
/// <returns>Instance of T or null if not found</returns>
|
||||
/// <exception cref="InvalidOperationException">The input sequence contains more than one element.-or-The input sequence is empty.</exception>
|
||||
T GetService<T>(Predicate<T> filter);
|
||||
|
||||
/// <summary>
|
||||
/// Gets multiple services of a given type
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns>An enumerable of matching services</returns>
|
||||
IEnumerable<T> GetServices<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets multiple services of a given type, where they match a filter
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="filter"></param>
|
||||
/// <returns></returns>
|
||||
IEnumerable<T> GetServices<T>(Predicate<T> filter);
|
||||
}
|
||||
|
||||
|
||||
public abstract class ServiceProviderBase : IMultiServiceProvider
|
||||
{
|
||||
|
||||
public T GetService<T>()
|
||||
{
|
||||
return GetServices<T>().SingleOrDefault();
|
||||
}
|
||||
|
||||
public T GetService<T>(Predicate<T> filter)
|
||||
{
|
||||
Validate.IsNotNull(nameof(filter), filter);
|
||||
return GetServices<T>().SingleOrDefault(t => filter(t));
|
||||
}
|
||||
|
||||
public IEnumerable<T> GetServices<T>(Predicate<T> filter)
|
||||
{
|
||||
Validate.IsNotNull(nameof(filter), filter);
|
||||
return GetServices<T>().Where(t => filter(t));
|
||||
}
|
||||
|
||||
public virtual IEnumerable<T> GetServices<T>()
|
||||
{
|
||||
var services = GetServicesImpl<T>();
|
||||
if (services == null)
|
||||
{
|
||||
return Enumerable.Empty<T>();
|
||||
}
|
||||
|
||||
return services.Select(t =>
|
||||
{
|
||||
InitComposableService(t);
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
private void InitComposableService<T>(T t)
|
||||
{
|
||||
IComposableService c = t as IComposableService;
|
||||
c?.SetServiceProvider(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all services using the build in implementation
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns></returns>
|
||||
protected abstract IEnumerable<T> GetServicesImpl<T>();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.Extensibility
|
||||
{
|
||||
/// <summary>
|
||||
/// Standard Metadata needed for extensions.
|
||||
/// </summary>
|
||||
public interface IStandardMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension version. Should be in the format "1.0.0.0" or similar
|
||||
/// </summary>
|
||||
string Version { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Unique Id used to identify the export.
|
||||
/// </summary>
|
||||
string Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional Display name describing the export type
|
||||
/// </summary>
|
||||
string DisplayName { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Microsoft.SqlTools.Hosting.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Extensibility
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A service provider implementation that allows registering of specific services
|
||||
/// </summary>
|
||||
public class RegisteredServiceProvider : ServiceProviderBase
|
||||
{
|
||||
public delegate IEnumerable ServiceLookup();
|
||||
|
||||
protected readonly Dictionary<Type, ServiceLookup> services = new Dictionary<Type, ServiceLookup>();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a singular service to be returned during lookup
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns>this provider, to simplify fluent declarations</returns>
|
||||
/// <exception cref="ArgumentNullException">If service is null</exception>
|
||||
/// <exception cref="InvalidOperationException">If an existing service is already registered</exception>
|
||||
public virtual RegisteredServiceProvider RegisterSingleService<T>(T service)
|
||||
{
|
||||
Validate.IsNotNull(nameof(service), service);
|
||||
ThrowIfAlreadyRegistered<T>();
|
||||
services.Add(typeof(T), () => service.AsSingleItemEnumerable());
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a singular service to be returned during lookup
|
||||
/// </summary>
|
||||
/// <param name="type">
|
||||
/// Type or interface this service should be registed as. Any <see cref="IMultiServiceProvider.GetServices{T}"/> request
|
||||
/// for that type will return this service
|
||||
/// </param>
|
||||
/// <param name="service">service object to be added</param>
|
||||
/// <returns>this provider, to simplify fluent declarations</returns>
|
||||
/// <exception cref="ArgumentNullException">If service is null</exception>
|
||||
/// <exception cref="InvalidOperationException">If an existing service is already registered</exception>
|
||||
public virtual RegisteredServiceProvider RegisterSingleService(Type type, object service)
|
||||
{
|
||||
Validate.IsNotNull(nameof(type), type);
|
||||
Validate.IsNotNull(nameof(service), service);
|
||||
ThrowIfAlreadyRegistered(type);
|
||||
ThrowIfIncompatible(type, service);
|
||||
services.Add(type, service.AsSingleItemEnumerable);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a function that can look up multiple services
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <returns>this provider, to simplify fluent declarations</returns>
|
||||
/// <exception cref="ArgumentNullException">If <paramref name="serviceLookup"/> is null</exception>
|
||||
/// <exception cref="InvalidOperationException">If an existing service is already registered</exception>
|
||||
public virtual RegisteredServiceProvider Register<T>(Func<IEnumerable<T>> serviceLookup)
|
||||
{
|
||||
Validate.IsNotNull(nameof(serviceLookup), serviceLookup);
|
||||
ThrowIfAlreadyRegistered<T>();
|
||||
services.Add(typeof(T), () => serviceLookup());
|
||||
return this;
|
||||
}
|
||||
|
||||
public virtual void RegisterHostedServices()
|
||||
{
|
||||
// Register all hosted services the service provider for their requested service type.
|
||||
// This ensures that when searching for the ConnectionService (eg) you can get it
|
||||
// without searching for an IHosted service of type ConnectionService
|
||||
foreach (IHostedService service in GetServices<IHostedService>())
|
||||
{
|
||||
RegisterSingleService(service.ServiceType, service);
|
||||
}
|
||||
}
|
||||
|
||||
private void ThrowIfAlreadyRegistered<T>()
|
||||
{
|
||||
ThrowIfAlreadyRegistered(typeof(T));
|
||||
}
|
||||
|
||||
private void ThrowIfAlreadyRegistered(Type type)
|
||||
{
|
||||
if (services.ContainsKey(type))
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ServiceAlreadyRegistered, type.Name));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void ThrowIfIncompatible(Type type, object service)
|
||||
{
|
||||
if (!type.IsInstanceOfType(service))
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, SR.ServiceNotOfExpectedType, service.GetType().Name, type.Name));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override IEnumerable<T> GetServicesImpl<T>()
|
||||
{
|
||||
ServiceLookup serviceLookup;
|
||||
if (services.TryGetValue(typeof(T), out serviceLookup))
|
||||
{
|
||||
return serviceLookup().Cast<T>();
|
||||
}
|
||||
return Enumerable.Empty<T>();
|
||||
}
|
||||
}
|
||||
}
|
||||
59
src/Microsoft.SqlTools.Hosting.v2/ExtensibleServiceHost.cs
Normal file
59
src/Microsoft.SqlTools.Hosting.v2/ExtensibleServiceHost.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// 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.Generic;
|
||||
using Microsoft.SqlTools.Hosting.Channels;
|
||||
using Microsoft.SqlTools.Hosting.Extensibility;
|
||||
using Microsoft.SqlTools.Hosting.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting
|
||||
{
|
||||
public class ExtensibleServiceHost : ServiceHost
|
||||
{
|
||||
private readonly RegisteredServiceProvider serviceProvider;
|
||||
|
||||
#region Construction
|
||||
|
||||
public ExtensibleServiceHost(RegisteredServiceProvider provider, ChannelBase protocolChannel)
|
||||
: base(protocolChannel)
|
||||
{
|
||||
Validate.IsNotNull(nameof(provider), provider);
|
||||
|
||||
provider.RegisterSingleService<IServiceHost>(this);
|
||||
provider.RegisterHostedServices();
|
||||
|
||||
// Initialize all hosted services
|
||||
foreach (IHostedService service in provider.GetServices<IHostedService>())
|
||||
{
|
||||
service.InitializeService(this);
|
||||
}
|
||||
|
||||
serviceProvider = provider;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new service host intended to be used as a JSON RPC server. StdIn is used
|
||||
/// for receiving messages, StdOut is used for sending messages. Services will be
|
||||
/// discovered from the assemblies in the current directory listed in
|
||||
/// <paramref name="assembliesToInclude"/>
|
||||
/// </summary>
|
||||
/// <param name="directory">Directory to include assemblies from</param>
|
||||
/// <param name="assembliesToInclude">
|
||||
/// List of assembly names in the current directory to search for service exports
|
||||
/// </param>
|
||||
/// <returns>Service host as a JSON RPC server over StdI/O</returns>
|
||||
public static ExtensibleServiceHost CreateDefaultExtensibleServer(string directory, IList<string> assembliesToInclude)
|
||||
{
|
||||
Validate.IsNotNull(nameof(assembliesToInclude), assembliesToInclude);
|
||||
|
||||
ExtensionServiceProvider serviceProvider = ExtensionServiceProvider.CreateFromAssembliesInDirectory(directory, assembliesToInclude);
|
||||
return new ExtensibleServiceHost(serviceProvider, new StdioServerChannel());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public IMultiServiceProvider ServiceProvider => serviceProvider;
|
||||
}
|
||||
}
|
||||
31
src/Microsoft.SqlTools.Hosting.v2/IServiceHost.cs
Normal file
31
src/Microsoft.SqlTools.Hosting.v2/IServiceHost.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 System;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.DataProtocol.Contracts;
|
||||
using Microsoft.SqlTools.Hosting.Protocol;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for service hosts. Inherits interface requirements for JSON RPC hosts
|
||||
/// </summary>
|
||||
public interface IServiceHost : IJsonRpcHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers a task to be executed when the initialize event is received
|
||||
/// </summary>
|
||||
/// <param name="initializeCallback">Function to execute when the initialize event received</param>
|
||||
void RegisterInitializeTask(Func<InitializeParameters, IEventSender, Task> initializeCallback);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a task to be executed when the shutdown event is received, before the channel
|
||||
/// is closed
|
||||
/// </summary>
|
||||
/// <param name="shutdownCallback">Function to execute when the shutdown request is received</param>
|
||||
void RegisterShutdownTask(Func<object, IEventSender, Task> shutdownCallback);
|
||||
}
|
||||
}
|
||||
230
src/Microsoft.SqlTools.Hosting.v2/Localization/sr.cs
Executable file
230
src/Microsoft.SqlTools.Hosting.v2/Localization/sr.cs
Executable file
@@ -0,0 +1,230 @@
|
||||
// WARNING:
|
||||
// This file was generated by the Microsoft DataWarehouse String Resource Tool 1.37.0.0
|
||||
// from information in sr.strings
|
||||
// DO NOT MODIFY THIS FILE'S CONTENTS, THEY WILL BE OVERWRITTEN
|
||||
//
|
||||
namespace Microsoft.SqlTools.Hosting
|
||||
{
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
using System.Globalization;
|
||||
|
||||
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class SR
|
||||
{
|
||||
protected SR()
|
||||
{ }
|
||||
|
||||
public static CultureInfo Culture
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.Culture;
|
||||
}
|
||||
set
|
||||
{
|
||||
Keys.Culture = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static string ServiceAlreadyRegistered
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.ServiceAlreadyRegistered);
|
||||
}
|
||||
}
|
||||
|
||||
public static string MultipleServicesFound
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.MultipleServicesFound);
|
||||
}
|
||||
}
|
||||
|
||||
public static string IncompatibleServiceForExtensionLoader
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.IncompatibleServiceForExtensionLoader);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ServiceProviderNotSet
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.ServiceProviderNotSet);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ServiceNotFound
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.ServiceNotFound);
|
||||
}
|
||||
}
|
||||
|
||||
public static string ServiceNotOfExpectedType
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.ServiceNotOfExpectedType);
|
||||
}
|
||||
}
|
||||
|
||||
public static string HostingUnexpectedEndOfStream
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.HostingUnexpectedEndOfStream);
|
||||
}
|
||||
}
|
||||
|
||||
public static string HostingHeaderMissingColon
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.HostingHeaderMissingColon);
|
||||
}
|
||||
}
|
||||
|
||||
public static string HostingHeaderMissingContentLengthHeader
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.HostingHeaderMissingContentLengthHeader);
|
||||
}
|
||||
}
|
||||
|
||||
public static string HostingHeaderMissingContentLengthValue
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.HostingHeaderMissingContentLengthValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static string HostingJsonRpcHostAlreadyStarted
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.HostingJsonRpcHostAlreadyStarted);
|
||||
}
|
||||
}
|
||||
|
||||
public static string HostingJsonRpcHostNotStarted
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.HostingJsonRpcHostNotStarted);
|
||||
}
|
||||
}
|
||||
|
||||
public static string HostingJsonRpcVersionMissing
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.HostingJsonRpcVersionMissing);
|
||||
}
|
||||
}
|
||||
|
||||
public static string HostingMessageMissingMethod
|
||||
{
|
||||
get
|
||||
{
|
||||
return Keys.GetString(Keys.HostingMessageMissingMethod);
|
||||
}
|
||||
}
|
||||
|
||||
public static string HostingMethodHandlerDoesNotExist(string messageType, string method)
|
||||
{
|
||||
return Keys.GetString(Keys.HostingMethodHandlerDoesNotExist, messageType, method);
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
public class Keys
|
||||
{
|
||||
static ResourceManager resourceManager = new ResourceManager("Microsoft.SqlTools.Hosting.Localization.SR", typeof(SR).GetTypeInfo().Assembly);
|
||||
|
||||
static CultureInfo _culture = null;
|
||||
|
||||
|
||||
public const string ServiceAlreadyRegistered = "ServiceAlreadyRegistered";
|
||||
|
||||
|
||||
public const string MultipleServicesFound = "MultipleServicesFound";
|
||||
|
||||
|
||||
public const string IncompatibleServiceForExtensionLoader = "IncompatibleServiceForExtensionLoader";
|
||||
|
||||
|
||||
public const string ServiceProviderNotSet = "ServiceProviderNotSet";
|
||||
|
||||
|
||||
public const string ServiceNotFound = "ServiceNotFound";
|
||||
|
||||
|
||||
public const string ServiceNotOfExpectedType = "ServiceNotOfExpectedType";
|
||||
|
||||
|
||||
public const string HostingUnexpectedEndOfStream = "HostingUnexpectedEndOfStream";
|
||||
|
||||
|
||||
public const string HostingHeaderMissingColon = "HostingHeaderMissingColon";
|
||||
|
||||
|
||||
public const string HostingHeaderMissingContentLengthHeader = "HostingHeaderMissingContentLengthHeader";
|
||||
|
||||
|
||||
public const string HostingHeaderMissingContentLengthValue = "HostingHeaderMissingContentLengthValue";
|
||||
|
||||
|
||||
public const string HostingJsonRpcHostAlreadyStarted = "HostingJsonRpcHostAlreadyStarted";
|
||||
|
||||
|
||||
public const string HostingJsonRpcHostNotStarted = "HostingJsonRpcHostNotStarted";
|
||||
|
||||
|
||||
public const string HostingJsonRpcVersionMissing = "HostingJsonRpcVersionMissing";
|
||||
|
||||
|
||||
public const string HostingMessageMissingMethod = "HostingMessageMissingMethod";
|
||||
|
||||
|
||||
public const string HostingMethodHandlerDoesNotExist = "HostingMethodHandlerDoesNotExist";
|
||||
|
||||
|
||||
private Keys()
|
||||
{ }
|
||||
|
||||
public static CultureInfo Culture
|
||||
{
|
||||
get
|
||||
{
|
||||
return _culture;
|
||||
}
|
||||
set
|
||||
{
|
||||
_culture = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetString(string key)
|
||||
{
|
||||
return resourceManager.GetString(key, _culture);
|
||||
}
|
||||
|
||||
|
||||
public static string GetString(string key, object arg0, object arg1)
|
||||
{
|
||||
return string.Format(global::System.Globalization.CultureInfo.CurrentCulture, resourceManager.GetString(key, _culture), arg0, arg1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
181
src/Microsoft.SqlTools.Hosting.v2/Localization/sr.resx
Executable file
181
src/Microsoft.SqlTools.Hosting.v2/Localization/sr.resx
Executable file
@@ -0,0 +1,181 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype=">text/microsoft-resx</resheader>
|
||||
<resheader name="version=">2.0</resheader>
|
||||
<resheader name="reader=">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer=">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1="><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing=">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64=">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64=">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata=">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true=">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded=">
|
||||
<xsd:element name="metadata=">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly=">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data=">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader=">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="ServiceAlreadyRegistered" xml:space="preserve">
|
||||
<value>Cannot register service for type {0}, one or more services already registered</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="MultipleServicesFound" xml:space="preserve">
|
||||
<value>Multiple services found for type {0}, expected only 1</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="IncompatibleServiceForExtensionLoader" xml:space="preserve">
|
||||
<value>Service of type {0} cannot be created by ExtensionLoader<{1}></value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="ServiceProviderNotSet" xml:space="preserve">
|
||||
<value>SetServiceProvider() was not called to establish the required service provider</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="ServiceNotFound" xml:space="preserve">
|
||||
<value>Service {0} was not found in the service provider</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="ServiceNotOfExpectedType" xml:space="preserve">
|
||||
<value>Service of Type {0} is not compatible with registered Type {1}</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="HostingUnexpectedEndOfStream" xml:space="preserve">
|
||||
<value>MessageReader's input stream ended unexpectedly, terminating</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="HostingHeaderMissingColon" xml:space="preserve">
|
||||
<value>Message header must separate key and value using ':'</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="HostingHeaderMissingContentLengthHeader" xml:space="preserve">
|
||||
<value>Fatal error: Content-Length header must be provided</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="HostingHeaderMissingContentLengthValue" xml:space="preserve">
|
||||
<value>Fatal error: Content-Length value is not an integer</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="HostingJsonRpcHostAlreadyStarted" xml:space="preserve">
|
||||
<value>JSON RPC host has already started</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="HostingJsonRpcHostNotStarted" xml:space="preserve">
|
||||
<value>JSON RPC host has not started</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="HostingJsonRpcVersionMissing" xml:space="preserve">
|
||||
<value>JSON RPC version parameter is missing or invalid</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="HostingMessageMissingMethod" xml:space="preserve">
|
||||
<value>JSON RPC message is missing required method parameter</value>
|
||||
<comment></comment>
|
||||
</data>
|
||||
<data name="HostingMethodHandlerDoesNotExist" xml:space="preserve">
|
||||
<value>{0} handler for method '{1}' does not exist.</value>
|
||||
<comment>.
|
||||
Parameters: 0 - messageType (string), 1 - method (string) </comment>
|
||||
</data>
|
||||
</root>
|
||||
57
src/Microsoft.SqlTools.Hosting.v2/Localization/sr.strings
Normal file
57
src/Microsoft.SqlTools.Hosting.v2/Localization/sr.strings
Normal file
@@ -0,0 +1,57 @@
|
||||
# String resource file
|
||||
#
|
||||
# When processed by the String Resource Tool, this file generates
|
||||
# both a .CS and a .RESX file with the same name as the file.
|
||||
# The .CS file contains a class which can be used to access these
|
||||
# string resources, including the ability to format in
|
||||
# parameters, which are identified with the .NET {x} format
|
||||
# (see String.Format help).
|
||||
#
|
||||
# Comments below assume the file name is SR.strings.
|
||||
#
|
||||
# Lines starting with a semicolon ";" are also treated as comments, but
|
||||
# in a future version they will be extracted and made available in LocStudio
|
||||
# Put your comments to localizers _before_ the string they apply to.
|
||||
#
|
||||
# SMO build specific comment
|
||||
# after generating the .resx file, run srgen on it and get the .resx file
|
||||
# please remember to also check that .resx in, along with the
|
||||
# .strings and .cs files
|
||||
|
||||
[strings]
|
||||
|
||||
############################################################################
|
||||
# Extensibility
|
||||
|
||||
ServiceAlreadyRegistered = Cannot register service for type {0}, one or more services already registered
|
||||
|
||||
MultipleServicesFound = Multiple services found for type {0}, expected only 1
|
||||
|
||||
IncompatibleServiceForExtensionLoader = Service of type {0} cannot be created by ExtensionLoader<{1}>
|
||||
|
||||
ServiceProviderNotSet = SetServiceProvider() was not called to establish the required service provider
|
||||
|
||||
ServiceNotFound = Service {0} was not found in the service provider
|
||||
|
||||
ServiceNotOfExpectedType = Service of Type {0} is not compatible with registered Type {1}
|
||||
|
||||
############################################################################
|
||||
# Hosting
|
||||
|
||||
HostingUnexpectedEndOfStream = MessageReader's input stream ended unexpectedly, terminating
|
||||
|
||||
HostingHeaderMissingColon = Message header must separate key and value using ':'
|
||||
|
||||
HostingHeaderMissingContentLengthHeader = Fatal error: Content-Length header must be provided
|
||||
|
||||
HostingHeaderMissingContentLengthValue = Fatal error: Content-Length value is not an integer
|
||||
|
||||
HostingJsonRpcHostAlreadyStarted = JSON RPC host has already started
|
||||
|
||||
HostingJsonRpcHostNotStarted = JSON RPC host has not started
|
||||
|
||||
HostingJsonRpcVersionMissing = JSON RPC version parameter is missing or invalid
|
||||
|
||||
HostingMessageMissingMethod = JSON RPC message is missing required method parameter
|
||||
|
||||
HostingMethodHandlerDoesNotExist(string messageType, string method) = {0} handler for method '{1}' does not exist.
|
||||
83
src/Microsoft.SqlTools.Hosting.v2/Localization/sr.xlf
Normal file
83
src/Microsoft.SqlTools.Hosting.v2/Localization/sr.xlf
Normal file
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
|
||||
<file datatype="xml" original="sr.resx" source-language="en">
|
||||
<body>
|
||||
<trans-unit id="HostingUnexpectedEndOfStream">
|
||||
<source>MessageReader's input stream ended unexpectedly, terminating</source>
|
||||
<target state="new">MessageReader's input stream ended unexpectedly, terminating</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="HostingHeaderMissingColon">
|
||||
<source>Message header must separate key and value using ':'</source>
|
||||
<target state="new">Message header must separate key and value using ':'</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="HostingHeaderMissingContentLengthHeader">
|
||||
<source>Fatal error: Content-Length header must be provided</source>
|
||||
<target state="new">Fatal error: Content-Length header must be provided</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="HostingHeaderMissingContentLengthValue">
|
||||
<source>Fatal error: Content-Length value is not an integer</source>
|
||||
<target state="new">Fatal error: Content-Length value is not an integer</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="ServiceAlreadyRegistered">
|
||||
<source>Cannot register service for type {0}, one or more services already registered</source>
|
||||
<target state="new">Cannot register service for type {0}, one or more services already registered</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="MultipleServicesFound">
|
||||
<source>Multiple services found for type {0}, expected only 1</source>
|
||||
<target state="new">Multiple services found for type {0}, expected only 1</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="IncompatibleServiceForExtensionLoader">
|
||||
<source>Service of type {0} cannot be created by ExtensionLoader<{1}></source>
|
||||
<target state="new">Service of type {0} cannot be created by ExtensionLoader<{1}></target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="ServiceProviderNotSet">
|
||||
<source>SetServiceProvider() was not called to establish the required service provider</source>
|
||||
<target state="new">SetServiceProvider() was not called to establish the required service provider</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="ServiceNotFound">
|
||||
<source>Service {0} was not found in the service provider</source>
|
||||
<target state="new">Service {0} was not found in the service provider</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="ServiceNotOfExpectedType">
|
||||
<source>Service of Type {0} is not compatible with registered Type {1}</source>
|
||||
<target state="new">Service of Type {0} is not compatible with registered Type {1}</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="HostingJsonRpcVersionMissing">
|
||||
<source>JSON RPC version parameter is missing or invalid</source>
|
||||
<target state="new">JSON RPC version parameter is missing or invalid</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="HostingMessageMissingMethod">
|
||||
<source>JSON RPC message is missing required method parameter</source>
|
||||
<target state="new">JSON RPC message is missing required method parameter</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="HostingMethodHandlerDoesNotExist">
|
||||
<source>{0} handler for method '{1}' does not exist.</source>
|
||||
<target state="new">{0} handler for method '{1}' does not exist.</target>
|
||||
<note>.
|
||||
Parameters: 0 - messageType (string), 1 - method (string) </note>
|
||||
</trans-unit>
|
||||
<trans-unit id="HostingJsonRpcHostAlreadyStarted">
|
||||
<source>JSON RPC host has already started</source>
|
||||
<target state="new">JSON RPC host has already started</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
<trans-unit id="HostingJsonRpcHostNotStarted">
|
||||
<source>JSON RPC host has not started</source>
|
||||
<target state="new">JSON RPC host has not started</target>
|
||||
<note></note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<AssemblyName>Microsoft.SqlTools.Hosting.v2</AssemblyName>
|
||||
<OutputType>Library</OutputType>
|
||||
<PackageDescription>
|
||||
The Microsoft.SqlTools.Hosting framework can host applications implementing the VSCode Language Server Protocol and/or
|
||||
applications implementing the Database Management Protocol. It handles service discovery, initialization, and communication over
|
||||
the JSON-RPC protocol.
|
||||
</PackageDescription>
|
||||
<Description>$(PackageDescription)</Description>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.0.4" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
|
||||
<PackageReference Include="System.Composition" Version="1.1.0" />
|
||||
<PackageReference Include="System.Runtime.Loader" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Localization\transXliff" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.SqlTools.DataProtocol.Contracts\Microsoft.SqlTools.DataProtocol.Contracts.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.SqlTools.Hosting.Contracts\Microsoft.SqlTools.Hosting.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\sqltools.common.targets" />
|
||||
</Project>
|
||||
46
src/Microsoft.SqlTools.Hosting.v2/Properties/AssemblyInfo.cs
Normal file
46
src/Microsoft.SqlTools.Hosting.v2/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("SqlTools Hosting Library")]
|
||||
[assembly: AssemblyDescription("Provides hosting services for SqlTools applications.")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("Microsoft")]
|
||||
[assembly: AssemblyProduct("SqlTools Hosting Library")]
|
||||
[assembly: AssemblyCopyright("<22> Microsoft Corporation. All rights reserved.")]
|
||||
[assembly: AssemblyTrademark("")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("6ECAFE73-131A-4221-AA13-C9BDE07FD92B")]
|
||||
|
||||
// Version information for an assembly consists of the following four values:
|
||||
//
|
||||
// Major Version
|
||||
// Minor Version
|
||||
// Build Number
|
||||
// Revision
|
||||
//
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.0.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||
[assembly: AssemblyInformationalVersion("1.0.0.0")]
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.SqlTools.Hosting.UnitTests")]
|
||||
[assembly: InternalsVisibleTo("Microsoft.SqlTools.CoreServices")]
|
||||
|
||||
32
src/Microsoft.SqlTools.Hosting.v2/Protocol/EventContext.cs
Normal file
32
src/Microsoft.SqlTools.Hosting.v2/Protocol/EventContext.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
34
src/Microsoft.SqlTools.Hosting.v2/Protocol/Exceptions.cs
Normal file
34
src/Microsoft.SqlTools.Hosting.v2/Protocol/Exceptions.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 System;
|
||||
|
||||
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))
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/Microsoft.SqlTools.Hosting.v2/Protocol/IEventSender.cs
Normal file
23
src/Microsoft.SqlTools.Hosting.v2/Protocol/IEventSender.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 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);
|
||||
}
|
||||
}
|
||||
28
src/Microsoft.SqlTools.Hosting.v2/Protocol/IJsonRpcHost.cs
Normal file
28
src/Microsoft.SqlTools.Hosting.v2/Protocol/IJsonRpcHost.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
27
src/Microsoft.SqlTools.Hosting.v2/Protocol/IRequestSender.cs
Normal file
27
src/Microsoft.SqlTools.Hosting.v2/Protocol/IRequestSender.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
446
src/Microsoft.SqlTools.Hosting.v2/Protocol/JsonRpcHost.cs
Normal file
446
src/Microsoft.SqlTools.Hosting.v2/Protocol/JsonRpcHost.cs
Normal file
@@ -0,0 +1,446 @@
|
||||
//
|
||||
// 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.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;
|
||||
|
||||
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 = string.Format("Exception occurred while receiving input message: {0}", e.Message);
|
||||
Logger.Instance.Write(LogLevel.Error, message);
|
||||
|
||||
// TODO: Add event to output queue, and unit test it
|
||||
|
||||
// Continue the loop
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verbose logging
|
||||
string logMessage = string.Format("Received message of type[{0}] and method[{1}]",
|
||||
incomingMessage.MessageType, incomingMessage.Method);
|
||||
Logger.Instance.Write(LogLevel.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 = string.Format("Failed to find method handler for type[{0}] and method[{1}]",
|
||||
incomingMessage.MessageType, incomingMessage.Method);
|
||||
Logger.Instance.Write(LogLevel.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 = string.Format("Exception thrown when handling message of type[{0}] and method[{1}]: {2}",
|
||||
incomingMessage.MessageType, incomingMessage.Method, e);
|
||||
Logger.Instance.Write(LogLevel.Error, geLogMessage);
|
||||
// TODO: Should we be returning a response for failing requests?
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Instance.Write(LogLevel.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.Instance.Write(LogLevel.Error, message);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Send the message
|
||||
string logMessage = string.Format("Sending message of type[{0}] and method[{1}]",
|
||||
outgoingMessage.MessageType, outgoingMessage.Method);
|
||||
Logger.Instance.Write(LogLevel.Verbose, logMessage);
|
||||
|
||||
await protocolChannel.MessageWriter.WriteMessage(outgoingMessage);
|
||||
}
|
||||
Logger.Instance.Write(LogLevel.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
|
||||
}
|
||||
}
|
||||
251
src/Microsoft.SqlTools.Hosting.v2/Protocol/Message.cs
Normal file
251
src/Microsoft.SqlTools.Hosting.v2/Protocol/Message.cs
Normal file
@@ -0,0 +1,251 @@
|
||||
//
|
||||
// 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 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
|
||||
}
|
||||
}
|
||||
257
src/Microsoft.SqlTools.Hosting.v2/Protocol/MessageReader.cs
Normal file
257
src/Microsoft.SqlTools.Hosting.v2/Protocol/MessageReader.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
59
src/Microsoft.SqlTools.Hosting.v2/Protocol/MessageWriter.cs
Normal file
59
src/Microsoft.SqlTools.Hosting.v2/Protocol/MessageWriter.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
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.Instance.Write(LogLevel.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/Microsoft.SqlTools.Hosting.v2/Protocol/RequestContext.cs
Normal file
57
src/Microsoft.SqlTools.Hosting.v2/Protocol/RequestContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
210
src/Microsoft.SqlTools.Hosting.v2/ServiceHost.cs
Normal file
210
src/Microsoft.SqlTools.Hosting.v2/ServiceHost.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
//
|
||||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.DataProtocol.Contracts;
|
||||
using Microsoft.SqlTools.DataProtocol.Contracts.ServerCapabilities;
|
||||
using Microsoft.SqlTools.Hosting.Channels;
|
||||
using Microsoft.SqlTools.Hosting.Contracts;
|
||||
using Microsoft.SqlTools.Hosting.Contracts.Internal;
|
||||
using Microsoft.SqlTools.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.Hosting.Utility;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting
|
||||
{
|
||||
public class ServiceHost : IServiceHost
|
||||
{
|
||||
private const int DefaultShutdownTimeoutSeconds = 120;
|
||||
|
||||
#region Fields
|
||||
|
||||
private int? shutdownTimeoutSeconds;
|
||||
|
||||
internal readonly List<Func<InitializeParameters, IEventSender, Task>> initCallbacks;
|
||||
internal readonly List<Func<object, IEventSender, Task>> shutdownCallbacks;
|
||||
internal IJsonRpcHost jsonRpcHost;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Construction
|
||||
|
||||
/// <summary>
|
||||
/// Base constructor
|
||||
/// </summary>
|
||||
internal ServiceHost()
|
||||
{
|
||||
shutdownCallbacks = new List<Func<object, IEventSender, Task>>();
|
||||
initCallbacks = new List<Func<InitializeParameters, IEventSender, Task>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new service host that with ability to provide custom protocol channels
|
||||
/// </summary>
|
||||
/// <param name="protocolChannel">Channel to use for JSON RPC input/output</param>
|
||||
public ServiceHost(ChannelBase protocolChannel)
|
||||
: this()
|
||||
{
|
||||
Validate.IsNotNull(nameof(protocolChannel), protocolChannel);
|
||||
jsonRpcHost = new JsonRpcHost(protocolChannel);
|
||||
|
||||
// Register any request that the service host will handle
|
||||
SetEventHandler(ExitNotification.Type, HandleExitNotification, true);
|
||||
SetAsyncRequestHandler(ShutdownRequest.Type, HandleShutdownRequest, true);
|
||||
SetAsyncRequestHandler(InitializeRequest.Type, HandleInitializeRequest, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new service host intended to be used as a JSON RPC server. StdIn is used
|
||||
/// for receiving messages, StdOut is used for sending messages.
|
||||
/// </summary>
|
||||
/// <returns>Service host as a JSON RPC server over StdI/O</returns>
|
||||
public static ServiceHost CreateDefaultServer()
|
||||
{
|
||||
return new ServiceHost(new StdioServerChannel());
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
public int ShutdownTimeoutSeconds
|
||||
{
|
||||
get => shutdownTimeoutSeconds ?? DefaultShutdownTimeoutSeconds;
|
||||
set => shutdownTimeoutSeconds = value;
|
||||
}
|
||||
|
||||
public InitializeResponse InitializeResponse { get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
#region IServiceHost Implementations
|
||||
|
||||
public void RegisterShutdownTask(Func<object, IEventSender, Task> shutdownCallback)
|
||||
{
|
||||
Validate.IsNotNull(nameof(shutdownCallback), shutdownCallback);
|
||||
shutdownCallbacks.Add(shutdownCallback);
|
||||
}
|
||||
|
||||
public void RegisterInitializeTask(Func<InitializeParameters, IEventSender, Task> initializeCallback)
|
||||
{
|
||||
Validate.IsNotNull(nameof(initializeCallback), initializeCallback);
|
||||
initCallbacks.Add(initializeCallback);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region IJsonRpcHost Implementation
|
||||
|
||||
public void SendEvent<TParams>(
|
||||
EventType<TParams> eventType,
|
||||
TParams eventParams)
|
||||
{
|
||||
jsonRpcHost.SendEvent(eventType, eventParams);
|
||||
}
|
||||
|
||||
public Task<TResult> SendRequest<TParams, TResult>(
|
||||
RequestType<TParams, TResult> requestType,
|
||||
TParams requestParams)
|
||||
{
|
||||
return jsonRpcHost.SendRequest(requestType, requestParams);
|
||||
}
|
||||
|
||||
public void SetAsyncEventHandler<TParams>(
|
||||
EventType<TParams> eventType,
|
||||
Func<TParams, EventContext, Task> eventHandler,
|
||||
bool overrideExisting)
|
||||
{
|
||||
jsonRpcHost.SetAsyncEventHandler(eventType, eventHandler, overrideExisting);
|
||||
}
|
||||
|
||||
public void SetEventHandler<TParams>(
|
||||
EventType<TParams> eventType,
|
||||
Action<TParams, EventContext> eventHandler,
|
||||
bool overrideExisting)
|
||||
{
|
||||
jsonRpcHost.SetEventHandler(eventType, eventHandler, overrideExisting);
|
||||
}
|
||||
|
||||
public void SetAsyncRequestHandler<TParams, TResult>(
|
||||
RequestType<TParams, TResult> requestType,
|
||||
Func<TParams, RequestContext<TResult>, Task> requestHandler,
|
||||
bool overrideExisting)
|
||||
{
|
||||
jsonRpcHost.SetAsyncRequestHandler(requestType, requestHandler, overrideExisting);
|
||||
}
|
||||
|
||||
public void SetRequestHandler<TParams, TResult>(
|
||||
RequestType<TParams, TResult> requestType,
|
||||
Action<TParams, RequestContext<TResult>> requestHandler,
|
||||
bool overrideExisting)
|
||||
{
|
||||
jsonRpcHost.SetRequestHandler(requestType, requestHandler, overrideExisting);
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
// Start the host
|
||||
jsonRpcHost.Start();
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
jsonRpcHost.Stop();
|
||||
}
|
||||
|
||||
public void WaitForExit()
|
||||
{
|
||||
jsonRpcHost.WaitForExit();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Request Handlers
|
||||
|
||||
internal async Task HandleInitializeRequest(InitializeParameters initParams, RequestContext<InitializeResponse> requestContext)
|
||||
{
|
||||
Logger.Instance.Write(LogLevel.Normal, "Service host received intialize request");
|
||||
|
||||
// Call all initialize methods provided by the service components
|
||||
IEnumerable<Task> initializeTasks = initCallbacks.Select(t => t(initParams, requestContext));
|
||||
|
||||
// Respond to initialize once all tasks are completed
|
||||
await Task.WhenAll(initializeTasks);
|
||||
|
||||
if (InitializeResponse == null)
|
||||
{
|
||||
InitializeResponse = new InitializeResponse
|
||||
{
|
||||
Capabilities = new ServerCapabilities()
|
||||
};
|
||||
}
|
||||
requestContext.SendResult(InitializeResponse);
|
||||
}
|
||||
|
||||
internal void HandleExitNotification(object exitParams, EventContext eventContext)
|
||||
{
|
||||
// Stop the server channel
|
||||
Stop();
|
||||
}
|
||||
|
||||
internal async Task HandleShutdownRequest(object shutdownParams, RequestContext<object> requestContext)
|
||||
{
|
||||
Logger.Instance.Write(LogLevel.Normal, "Service host received shutdown request");
|
||||
|
||||
// Call all the shutdown methods provided by the service components
|
||||
IEnumerable<Task> shutdownTasks = shutdownCallbacks.Select(t => t(shutdownParams, requestContext));
|
||||
|
||||
// Shutdown once all tasks are completed, or after the timeout expires, whichever comes first
|
||||
TimeSpan shutdownTimeout = TimeSpan.FromSeconds(ShutdownTimeoutSeconds);
|
||||
await Task.WhenAny(Task.WhenAll(shutdownTasks), Task.Delay(shutdownTimeout));
|
||||
requestContext.SendResult(null);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
98
src/Microsoft.SqlTools.Hosting.v2/Utility/AsyncLock.cs
Normal file
98
src/Microsoft.SqlTools.Hosting.v2/Utility/AsyncLock.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
//
|
||||
// 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;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a simple wrapper over a SemaphoreSlim to allow
|
||||
/// synchronization locking inside of async calls. Cannot be
|
||||
/// used recursively.
|
||||
/// </summary>
|
||||
public class AsyncLock
|
||||
{
|
||||
#region Fields
|
||||
|
||||
private readonly Task<IDisposable> lockReleaseTask;
|
||||
private readonly SemaphoreSlim lockSemaphore = new SemaphoreSlim(1, 1);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Constructors
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the AsyncLock class.
|
||||
/// </summary>
|
||||
public AsyncLock()
|
||||
{
|
||||
lockReleaseTask = Task.FromResult((IDisposable)new LockReleaser(this));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Methods
|
||||
|
||||
/// <summary>
|
||||
/// Locks
|
||||
/// </summary>
|
||||
/// <returns>A task which has an IDisposable</returns>
|
||||
public Task<IDisposable> LockAsync()
|
||||
{
|
||||
return LockAsync(CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Obtains or waits for a lock which can be used to synchronize
|
||||
/// access to a resource. The wait may be cancelled with the
|
||||
/// given CancellationToken.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">
|
||||
/// A CancellationToken which can be used to cancel the lock.
|
||||
/// </param>
|
||||
/// <returns></returns>
|
||||
public Task<IDisposable> LockAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Task waitTask = lockSemaphore.WaitAsync(cancellationToken);
|
||||
|
||||
return waitTask.IsCompleted
|
||||
? lockReleaseTask
|
||||
: waitTask.ContinueWith(
|
||||
(t, releaser) => (IDisposable)releaser,
|
||||
lockReleaseTask.Result,
|
||||
cancellationToken,
|
||||
TaskContinuationOptions.ExecuteSynchronously,
|
||||
TaskScheduler.Default);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Private Classes
|
||||
|
||||
/// <summary>
|
||||
/// Provides an IDisposable wrapper around an AsyncLock so
|
||||
/// that it can easily be used inside of a 'using' block.
|
||||
/// </summary>
|
||||
private class LockReleaser : IDisposable
|
||||
{
|
||||
private readonly AsyncLock lockToRelease;
|
||||
|
||||
internal LockReleaser(AsyncLock lockToRelease)
|
||||
{
|
||||
this.lockToRelease = lockToRelease;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lockToRelease.lockSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
164
src/Microsoft.SqlTools.Hosting.v2/Utility/CommandOptions.cs
Normal file
164
src/Microsoft.SqlTools.Hosting.v2/Utility/CommandOptions.cs
Normal file
@@ -0,0 +1,164 @@
|
||||
//
|
||||
// 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.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// The command-line options helper class.
|
||||
/// </summary>
|
||||
public class CommandOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Construct and parse command line options from the arguments array
|
||||
/// </summary>
|
||||
public CommandOptions(string[] args, string serviceName)
|
||||
{
|
||||
ServiceName = serviceName;
|
||||
ErrorMessage = string.Empty;
|
||||
Locale = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < args.Length; ++i)
|
||||
{
|
||||
string arg = args[i];
|
||||
if (arg.StartsWith("--") || arg.StartsWith("-"))
|
||||
{
|
||||
// Extracting arguments and properties
|
||||
arg = arg.Substring(1).ToLowerInvariant();
|
||||
string argName = arg;
|
||||
|
||||
switch (argName)
|
||||
{
|
||||
case "-enable-logging":
|
||||
EnableLogging = true;
|
||||
break;
|
||||
case "-log-dir":
|
||||
SetLoggingDirectory(args[++i]);
|
||||
break;
|
||||
case "-locale":
|
||||
SetLocale(args[++i]);
|
||||
break;
|
||||
case "h":
|
||||
case "-help":
|
||||
ShouldExit = true;
|
||||
return;
|
||||
default:
|
||||
ErrorMessage += string.Format("Unknown argument \"{0}\"" + Environment.NewLine, argName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage += ex.ToString();
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!string.IsNullOrEmpty(ErrorMessage) || ShouldExit)
|
||||
{
|
||||
Console.WriteLine(Usage);
|
||||
ShouldExit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains any error messages during execution
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; private set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Whether diagnostic logging is enabled
|
||||
/// </summary>
|
||||
public bool EnableLogging { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the directory where log files are output.
|
||||
/// </summary>
|
||||
public string LoggingDirectory { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the program should exit immediately. Set to true when the usage is printed.
|
||||
/// </summary>
|
||||
public bool ShouldExit { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The locale our we should instantiate this service in
|
||||
/// </summary>
|
||||
public string Locale { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of service that is receiving command options
|
||||
/// </summary>
|
||||
public string ServiceName { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the usage string describing command-line arguments for the program
|
||||
/// </summary>
|
||||
public string Usage
|
||||
{
|
||||
get
|
||||
{
|
||||
var str = string.Format("{0}" + Environment.NewLine +
|
||||
ServiceName + " " + Environment.NewLine +
|
||||
" Options:" + Environment.NewLine +
|
||||
" [--enable-logging]" + Environment.NewLine +
|
||||
" [--log-dir **] (default: current directory)" + Environment.NewLine +
|
||||
" [--help]" + Environment.NewLine +
|
||||
" [--locale **] (default: 'en')" + Environment.NewLine,
|
||||
ErrorMessage);
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
private void SetLoggingDirectory(string loggingDirectory)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(loggingDirectory))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.LoggingDirectory = Path.GetFullPath(loggingDirectory);
|
||||
}
|
||||
|
||||
public virtual void SetLocale(string locale)
|
||||
{
|
||||
try
|
||||
{
|
||||
LocaleSetter(locale);
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
// Ignore CultureNotFoundException since it only is thrown before Windows 10. Windows 10,
|
||||
// along with macOS and Linux, pick up the default culture if an invalid locale is passed
|
||||
// into the CultureInfo constructor.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the Locale field used for testing and also sets the global CultureInfo used for
|
||||
/// culture-specific messages
|
||||
/// </summary>
|
||||
/// <param name="locale"></param>
|
||||
internal void LocaleSetter(string locale)
|
||||
{
|
||||
// Creating cultureInfo from our given locale
|
||||
CultureInfo language = new CultureInfo(locale);
|
||||
Locale = locale;
|
||||
|
||||
// Setting our language globally
|
||||
CultureInfo.CurrentCulture = language;
|
||||
CultureInfo.CurrentUICulture = language;
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/Microsoft.SqlTools.Hosting.v2/Utility/Extensions.cs
Normal file
100
src/Microsoft.SqlTools.Hosting.v2/Utility/Extensions.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Utility
|
||||
{
|
||||
public static class ObjectExtensions
|
||||
{
|
||||
|
||||
public static IEnumerable<T> AsSingleItemEnumerable<T>(this T obj)
|
||||
{
|
||||
yield return obj;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension to evaluate an object's ToString() method in an exception safe way. This will
|
||||
/// extension method will not throw.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object on which to call ToString()</param>
|
||||
/// <returns>The ToString() return value or a suitable error message is that throws.</returns>
|
||||
public static string SafeToString(this object obj)
|
||||
{
|
||||
string str;
|
||||
|
||||
try
|
||||
{
|
||||
str = obj.ToString();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
str = $"<Error converting poperty value to string - {ex.Message}>";
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a boolean to a "1" or "0" string. Particularly helpful when sending telemetry
|
||||
/// </summary>
|
||||
public static string ToOneOrZeroString(this bool isTrue)
|
||||
{
|
||||
return isTrue ? "1" : "0";
|
||||
}
|
||||
}
|
||||
|
||||
public static class NullableExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension method to evaluate a bool? and determine if it has the value and is true.
|
||||
/// This way we avoid throwing if the bool? doesn't have a value.
|
||||
/// </summary>
|
||||
/// <param name="obj">The <c>bool?</c> to process</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if <paramref name="obj"/> has a value and it is <c>true</c>
|
||||
/// <c>false</c> otherwise.
|
||||
/// </returns>
|
||||
public static bool HasTrue(this bool? obj)
|
||||
{
|
||||
return obj.HasValue && obj.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExceptionExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns true if the passed exception or any inner exception is an OperationCanceledException instance.
|
||||
/// </summary>
|
||||
public static bool IsOperationCanceledException(this Exception e)
|
||||
{
|
||||
Exception current = e;
|
||||
while (current != null)
|
||||
{
|
||||
if (current is OperationCanceledException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
current = current.InnerException;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CollectionExtensions {
|
||||
|
||||
public static TValue GetValueOrSpecifiedDefault<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> map, TKey key)
|
||||
{
|
||||
if (map != null && map.ContainsKey(key))
|
||||
{
|
||||
return map[key];
|
||||
}
|
||||
return default(TValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
299
src/Microsoft.SqlTools.Hosting.v2/Utility/Logger.cs
Normal file
299
src/Microsoft.SqlTools.Hosting.v2/Utility/Logger.cs
Normal file
@@ -0,0 +1,299 @@
|
||||
//
|
||||
// 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 System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the level indicators for log messages.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates a verbose log message.
|
||||
/// </summary>
|
||||
Verbose,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a normal, non-verbose log message.
|
||||
/// </summary>
|
||||
Normal,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a warning message.
|
||||
/// </summary>
|
||||
Warning,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates an error message.
|
||||
/// </summary>
|
||||
Error
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a simple logging interface. May be replaced with a
|
||||
/// more robust solution at a later date.
|
||||
/// </summary>
|
||||
public class Logger
|
||||
{
|
||||
private static LogWriter logWriter;
|
||||
|
||||
private static bool isEnabled;
|
||||
|
||||
private static bool isInitialized = false;
|
||||
|
||||
private static Lazy<Logger> lazyInstance = new Lazy<Logger>(() => new Logger());
|
||||
|
||||
public static Logger Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
return lazyInstance.Value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the Logger for the current session.
|
||||
/// </summary>
|
||||
/// <param name="logFilePath">
|
||||
/// Optional. Specifies the path at which log messages will be written.
|
||||
/// </param>
|
||||
/// <param name="minimumLogLevel">
|
||||
/// Optional. Specifies the minimum log message level to write to the log file.
|
||||
/// </param>
|
||||
public void Initialize(
|
||||
string logFilePath = "sqltools",
|
||||
LogLevel minimumLogLevel = LogLevel.Normal,
|
||||
bool isEnabled = true)
|
||||
{
|
||||
Logger.isEnabled = isEnabled;
|
||||
|
||||
// return if the logger is not enabled or already initialized
|
||||
if (!Logger.isEnabled || Logger.isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.isInitialized = true;
|
||||
|
||||
// Create the log directory
|
||||
string logDir = Path.GetDirectoryName(logFilePath);
|
||||
if (!string.IsNullOrWhiteSpace(logDir))
|
||||
{
|
||||
if (!Directory.Exists(logDir))
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(logDir);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Creating the log directory is a best effort operation, so ignore any failures.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// get a unique number to prevent conflicts of two process launching at the same time
|
||||
int uniqueId;
|
||||
try
|
||||
{
|
||||
uniqueId = Process.GetCurrentProcess().Id;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// if the pid look up fails for any reason, just use a random number
|
||||
uniqueId = new Random().Next(1000, 9999);
|
||||
}
|
||||
|
||||
// make the log path unique
|
||||
string fullFileName = string.Format(
|
||||
"{0}_{1,4:D4}{2,2:D2}{3,2:D2}{4,2:D2}{5,2:D2}{6,2:D2}{7}.log",
|
||||
logFilePath,
|
||||
DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day,
|
||||
DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second,
|
||||
uniqueId);
|
||||
|
||||
if (logWriter != null)
|
||||
{
|
||||
logWriter.Dispose();
|
||||
}
|
||||
|
||||
// TODO: Parameterize this
|
||||
logWriter =
|
||||
new LogWriter(
|
||||
minimumLogLevel,
|
||||
fullFileName,
|
||||
true);
|
||||
|
||||
Write(LogLevel.Normal, "Initializing SQL Tools Service Host logger");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the Logger.
|
||||
/// </summary>
|
||||
public void Close()
|
||||
{
|
||||
if (logWriter != null)
|
||||
{
|
||||
logWriter.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a message to the log file.
|
||||
/// </summary>
|
||||
/// <param name="logLevel">The level at which the message will be written.</param>
|
||||
/// <param name="logMessage">The message text to be written.</param>
|
||||
/// <param name="callerName">The name of the calling method.</param>
|
||||
/// <param name="callerSourceFile">The source file path where the calling method exists.</param>
|
||||
/// <param name="callerLineNumber">The line number of the calling method.</param>
|
||||
public void Write(
|
||||
LogLevel logLevel,
|
||||
string logMessage,
|
||||
[CallerMemberName] string callerName = null,
|
||||
[CallerFilePath] string callerSourceFile = null,
|
||||
[CallerLineNumber] int callerLineNumber = 0)
|
||||
{
|
||||
// return if the logger is not enabled or not initialized
|
||||
if (!Logger.isEnabled || !Logger.isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (logWriter != null)
|
||||
{
|
||||
logWriter.Write(
|
||||
logLevel,
|
||||
logMessage,
|
||||
callerName,
|
||||
callerSourceFile,
|
||||
callerLineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class LogWriter : IDisposable
|
||||
{
|
||||
private object logLock = new object();
|
||||
private TextWriter textWriter;
|
||||
private LogLevel minimumLogLevel = LogLevel.Verbose;
|
||||
|
||||
public LogWriter(LogLevel minimumLogLevel, string logFilePath, bool deleteExisting)
|
||||
{
|
||||
this.minimumLogLevel = minimumLogLevel;
|
||||
|
||||
// Ensure that we have a usable log file path
|
||||
if (!Path.IsPathRooted(logFilePath))
|
||||
{
|
||||
logFilePath =
|
||||
Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
logFilePath);
|
||||
}
|
||||
|
||||
|
||||
if (!this.TryOpenLogFile(logFilePath, deleteExisting))
|
||||
{
|
||||
// If the log file couldn't be opened at this location,
|
||||
// try opening it in a more reliable path
|
||||
this.TryOpenLogFile(
|
||||
Path.Combine(
|
||||
Environment.GetEnvironmentVariable("TEMP"),
|
||||
Path.GetFileName(logFilePath)),
|
||||
deleteExisting);
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(
|
||||
LogLevel logLevel,
|
||||
string logMessage,
|
||||
string callerName = null,
|
||||
string callerSourceFile = null,
|
||||
int callerLineNumber = 0)
|
||||
{
|
||||
if (this.textWriter != null &&
|
||||
logLevel >= this.minimumLogLevel)
|
||||
{
|
||||
// System.IO is not thread safe
|
||||
lock (this.logLock)
|
||||
{
|
||||
// Print the timestamp and log level
|
||||
this.textWriter.WriteLine(
|
||||
"{0} [{1}] - Method \"{2}\" at line {3} of {4}\r\n",
|
||||
DateTime.Now,
|
||||
logLevel.ToString().ToUpper(),
|
||||
callerName,
|
||||
callerLineNumber,
|
||||
callerSourceFile);
|
||||
|
||||
// Print out indented message lines
|
||||
foreach (var messageLine in logMessage.Split('\n'))
|
||||
{
|
||||
this.textWriter.WriteLine(" " + messageLine.TrimEnd());
|
||||
}
|
||||
|
||||
// Finish with a newline and flush the writer
|
||||
this.textWriter.WriteLine();
|
||||
this.textWriter.Flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (this.textWriter != null)
|
||||
{
|
||||
this.textWriter.Flush();
|
||||
this.textWriter.Dispose();
|
||||
this.textWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryOpenLogFile(
|
||||
string logFilePath,
|
||||
bool deleteExisting)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Make sure the log directory exists
|
||||
Directory.CreateDirectory(
|
||||
Path.GetDirectoryName(
|
||||
logFilePath));
|
||||
|
||||
// Open the log file for writing with UTF8 encoding
|
||||
this.textWriter =
|
||||
new StreamWriter(
|
||||
new FileStream(
|
||||
logFilePath,
|
||||
deleteExisting ?
|
||||
FileMode.Create :
|
||||
FileMode.Append),
|
||||
Encoding.UTF8);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (e is UnauthorizedAccessException ||
|
||||
e is IOException)
|
||||
{
|
||||
// This exception is thrown when we can't open the file
|
||||
// at the path in logFilePath. Return false to indicate
|
||||
// that the log file couldn't be created.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Unexpected exception, rethrow it
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// 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.SqlTools.Hosting.Utility
|
||||
{
|
||||
public class ServiceProviderNotSetException : InvalidOperationException {
|
||||
|
||||
public ServiceProviderNotSetException()
|
||||
: base(SR.ServiceProviderNotSet) {
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/Microsoft.SqlTools.Hosting.v2/Utility/TaskExtensions.cs
Normal file
99
src/Microsoft.SqlTools.Hosting.v2/Utility/TaskExtensions.cs
Normal file
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// 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.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Utility
|
||||
{
|
||||
public static class TaskExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds handling to check the Exception field of a task and log it if the task faulted
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will effectively swallow exceptions in the task chain.
|
||||
/// </remarks>
|
||||
/// <param name="antecedent">The task to continue</param>
|
||||
/// <param name="continuationAction">
|
||||
/// An optional operation to perform after exception handling has occurred
|
||||
/// </param>
|
||||
/// <returns>Task with exception handling on continuation</returns>
|
||||
public static Task ContinueWithOnFaulted(this Task antecedent, Action<Task> continuationAction)
|
||||
{
|
||||
return antecedent.ContinueWith(task =>
|
||||
{
|
||||
// If the task hasn't faulted or has an exception, skip processing
|
||||
if (!task.IsFaulted || task.Exception == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LogTaskExceptions(task.Exception);
|
||||
|
||||
// Run the continuation task that was provided
|
||||
try
|
||||
{
|
||||
continuationAction?.Invoke(task);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Instance.Write(LogLevel.Error, $"Exception in exception handling continuation: {e}");
|
||||
Logger.Instance.Write(LogLevel.Error, e.StackTrace);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds handling to check the Exception field of a task and log it if the task faulted.
|
||||
/// This version allows for async code to be ran in the continuation function.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will effectively swallow exceptions in the task chain.
|
||||
/// </remarks>
|
||||
/// <param name="antecedent">The task to continue</param>
|
||||
/// <param name="continuationFunc">
|
||||
/// An optional operation to perform after exception handling has occurred
|
||||
/// </param>
|
||||
/// <returns>Task with exception handling on continuation</returns>
|
||||
public static Task ContinueWithOnFaulted(this Task antecedent, Func<Task, Task> continuationFunc)
|
||||
{
|
||||
return antecedent.ContinueWith(task =>
|
||||
{
|
||||
// If the task hasn't faulted or doesn't have an exception, skip processing
|
||||
if (!task.IsFaulted || task.Exception == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
LogTaskExceptions(task.Exception);
|
||||
|
||||
// Run the continuation task that was provided
|
||||
try
|
||||
{
|
||||
continuationFunc?.Invoke(antecedent).Wait();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Instance.Write(LogLevel.Error, $"Exception in exception handling continuation: {e}");
|
||||
Logger.Instance.Write(LogLevel.Error, e.StackTrace);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void LogTaskExceptions(AggregateException exception)
|
||||
{
|
||||
// Construct an error message for an aggregate exception and log it
|
||||
StringBuilder sb = new StringBuilder("Unhandled exception(s) in async task:");
|
||||
foreach (Exception e in exception.InnerExceptions)
|
||||
{
|
||||
sb.AppendLine($"{e.GetType().Name}: {e.Message}");
|
||||
sb.AppendLine(e.StackTrace);
|
||||
}
|
||||
Logger.Instance.Write(LogLevel.Error, sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
158
src/Microsoft.SqlTools.Hosting.v2/Utility/Validate.cs
Normal file
158
src/Microsoft.SqlTools.Hosting.v2/Utility/Validate.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
namespace Microsoft.SqlTools.Hosting.Utility
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides common validation methods to simplify method
|
||||
/// parameter checks.
|
||||
/// </summary>
|
||||
public static class Validate
|
||||
{
|
||||
/// <summary>
|
||||
/// Throws ArgumentNullException if value is null.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter being validated.</param>
|
||||
/// <param name="valueToCheck">The value of the parameter being validated.</param>
|
||||
public static void IsNotNull(string parameterName, object valueToCheck)
|
||||
{
|
||||
if (valueToCheck == null)
|
||||
{
|
||||
throw new ArgumentNullException(parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws ArgumentOutOfRangeException if the value is outside
|
||||
/// of the given lower and upper limits.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter being validated.</param>
|
||||
/// <param name="valueToCheck">The value of the parameter being validated.</param>
|
||||
/// <param name="lowerLimit">The lower limit which the value should not be less than.</param>
|
||||
/// <param name="upperLimit">The upper limit which the value should not be greater than.</param>
|
||||
public static void IsWithinRange(
|
||||
string parameterName,
|
||||
long valueToCheck,
|
||||
long lowerLimit,
|
||||
long upperLimit)
|
||||
{
|
||||
// TODO: Debug assert here if lowerLimit >= upperLimit
|
||||
|
||||
if (valueToCheck < lowerLimit || valueToCheck > upperLimit)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
parameterName,
|
||||
valueToCheck,
|
||||
string.Format(
|
||||
"Value is not between {0} and {1}",
|
||||
lowerLimit,
|
||||
upperLimit));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws ArgumentOutOfRangeException if the value is greater than or equal
|
||||
/// to the given upper limit.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter being validated.</param>
|
||||
/// <param name="valueToCheck">The value of the parameter being validated.</param>
|
||||
/// <param name="upperLimit">The upper limit which the value should be less than.</param>
|
||||
public static void IsLessThan(
|
||||
string parameterName,
|
||||
long valueToCheck,
|
||||
long upperLimit)
|
||||
{
|
||||
if (valueToCheck >= upperLimit)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
parameterName,
|
||||
valueToCheck,
|
||||
string.Format(
|
||||
"Value is greater than or equal to {0}",
|
||||
upperLimit));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws ArgumentOutOfRangeException if the value is less than or equal
|
||||
/// to the given lower limit.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter being validated.</param>
|
||||
/// <param name="valueToCheck">The value of the parameter being validated.</param>
|
||||
/// <param name="lowerLimit">The lower limit which the value should be greater than.</param>
|
||||
public static void IsGreaterThan(
|
||||
string parameterName,
|
||||
long valueToCheck,
|
||||
long lowerLimit)
|
||||
{
|
||||
if (valueToCheck < lowerLimit)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(
|
||||
parameterName,
|
||||
valueToCheck,
|
||||
string.Format(
|
||||
"Value is less than or equal to {0}",
|
||||
lowerLimit));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws ArgumentException if the value is equal to the undesired value.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type of value to be validated.</typeparam>
|
||||
/// <param name="parameterName">The name of the parameter being validated.</param>
|
||||
/// <param name="undesiredValue">The value that valueToCheck should not equal.</param>
|
||||
/// <param name="valueToCheck">The value of the parameter being validated.</param>
|
||||
public static void IsNotEqual<TValue>(
|
||||
string parameterName,
|
||||
TValue valueToCheck,
|
||||
TValue undesiredValue)
|
||||
{
|
||||
if (EqualityComparer<TValue>.Default.Equals(valueToCheck, undesiredValue))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
string.Format(
|
||||
"The given value '{0}' should not equal '{1}'",
|
||||
valueToCheck,
|
||||
undesiredValue),
|
||||
parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws ArgumentException if the value is null or an empty string.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter being validated.</param>
|
||||
/// <param name="valueToCheck">The value of the parameter being validated.</param>
|
||||
public static void IsNotNullOrEmptyString(string parameterName, string valueToCheck)
|
||||
{
|
||||
if (string.IsNullOrEmpty(valueToCheck))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Parameter contains a null, empty, or whitespace string.",
|
||||
parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws ArgumentException if the value is null, an empty string,
|
||||
/// or a string containing only whitespace.
|
||||
/// </summary>
|
||||
/// <param name="parameterName">The name of the parameter being validated.</param>
|
||||
/// <param name="valueToCheck">The value of the parameter being validated.</param>
|
||||
public static void IsNotNullOrWhitespaceString(string parameterName, string valueToCheck)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(valueToCheck))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Parameter contains a null, empty, or whitespace string.",
|
||||
parameterName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user