Move unused forked code to external directory (#1192)

* Move unused forked code to external directory

* Fix SLN build errors

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

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

View File

@@ -0,0 +1,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
}
}

View 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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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; }
}
}

View File

@@ -0,0 +1,170 @@
//
// 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.Concurrent;
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 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>
/// Merges in new assemblies to the existing container configuration.
/// </summary>
public void AddAssembliesToConfiguration(IEnumerable<Assembly> assemblies)
{
Validate.IsNotNull(nameof(assemblies), assemblies);
var previousConfig = config;
this.config = conventions => {
// Chain in the existing configuration function's result, then include additional
// assemblies
ContainerConfiguration containerConfig = previousConfig(conventions);
return containerConfig.WithAssemblies(assemblies, conventions);
};
}
}
/// <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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,107 @@
//
// 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.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&lt;MyService&gt;
/// {
/// 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.Write(TraceEventType.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.Write(TraceEventType.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);
}
}

View File

@@ -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>();
}
}

View File

@@ -0,0 +1,28 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
namespace Microsoft.SqlTools.Hosting.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; }
}
}

View File

@@ -0,0 +1,121 @@
//
// 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;
using Microsoft.SqlTools.Hosting.v2;
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>();
}
}
}

View 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;
}
}

View File

@@ -0,0 +1,31 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using 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);
}
}

View File

@@ -0,0 +1,230 @@
// WARNING:
// This file was generated by the Microsoft DataWarehouse String Resource Tool 4.0.0.0
// from information in sr.strings
// DO NOT MODIFY THIS FILE'S CONTENTS, THEY WILL BE OVERWRITTEN
//
namespace Microsoft.SqlTools.Hosting.v2
{
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.v2.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);
}
}
}
}

View 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&lt;{1}&gt;</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&apos;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 &apos;:&apos;</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 &apos;{1}&apos; does not exist.</value>
<comment>.
Parameters: 0 - messageType (string), 1 - method (string) </comment>
</data>
</root>

View 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.

View 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&lt;{1}&gt;</source>
<target state="new">Service of type {0} cannot be created by ExtensionLoader&lt;{1}&gt;</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>

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<AssemblyName>Microsoft.SqlTools.Hosting.v2</AssemblyName>
<PackageId>Microsoft.SqlTools.Hosting.v2</PackageId>
<EnableDefaultEmbeddedResourceItems>false</EnableDefaultEmbeddedResourceItems>
<OutputType>Library</OutputType>
<RootNamespace>Microsoft.SqlTools.Hosting.v2</RootNamespace>
<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" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="System.Composition"/>
<PackageReference Include="System.Runtime.Loader" />
</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>
<ItemGroup>
<EmbeddedResource Include="Localization\sr.resx" />
<None Include="Localization\sr.strings" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,45 @@
//
// 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")]

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using Microsoft.SqlTools.Hosting.Contracts;
namespace Microsoft.SqlTools.Hosting.Protocol
{
/// <summary>
/// Interface for objects that can send events via the JSON RPC channel
/// </summary>
public interface IEventSender
{
/// <summary>
/// Sends an event over the JSON RPC channel
/// </summary>
/// <param name="eventType">Configuration of the event to send</param>
/// <param name="eventParams">Parameters for the event to send</param>
/// <typeparam name="TParams">Type of the parameters for the event, defined in <paramref name="eventType"/></typeparam>
void SendEvent<TParams>(EventType<TParams> eventType, TParams eventParams);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,27 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Contracts;
namespace Microsoft.SqlTools.Hosting.Protocol
{
/// <summary>
/// Interface for objects that can requests via the JSON RPC channel
/// </summary>
public interface IRequestSender
{
/// <summary>
/// Sends a request over the JSON RPC channel. It will wait for a response to the message
/// before completing.
/// </summary>
/// <param name="requestType">Configuration of the request to send</param>
/// <param name="requestParams">Parameters for the request to send</param>
/// <typeparam name="TParams">Type of the parameters for the request, defined by <paramref name="requestType"/></typeparam>
/// <typeparam name="TResult"></typeparam>
/// <returns></returns>
Task<TResult> SendRequest<TParams, TResult>(RequestType<TParams, TResult> requestType, TParams requestParams);
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,60 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Utility;
namespace Microsoft.SqlTools.Hosting.Protocol
{
public class MessageWriter
{
private const string ContentLength = "Content-Length: ";
private const string ContentType = "Content-Type: application/json";
private const string HeaderSeparator = "\r\n";
private const string HeaderEnd = "\r\n\r\n";
private readonly Stream outputStream;
private readonly AsyncLock writeLock = new AsyncLock();
public MessageWriter(Stream outputStream)
{
Validate.IsNotNull("streamWriter", outputStream);
this.outputStream = outputStream;
}
public virtual async Task WriteMessage(Message messageToWrite)
{
Validate.IsNotNull("messageToWrite", messageToWrite);
// Log the JSON representation of the message
string logMessage = string.Format("Sending message of type[{0}] and method[{1}]",
messageToWrite.MessageType, messageToWrite.Method);
Logger.Write(TraceEventType.Verbose, logMessage);
string serializedMessage = messageToWrite.Serialize();
// TODO: Allow encoding to be passed in
byte[] messageBytes = Encoding.UTF8.GetBytes(serializedMessage);
string headers = ContentLength + messageBytes.Length + HeaderSeparator
+ ContentType + HeaderEnd;
byte[] headerBytes = Encoding.ASCII.GetBytes(headers);
// Make sure only one call is writing at a time. You might be thinking
// "Why not use a normal lock?" We use an AsyncLock here so that the
// message loop doesn't get blocked while waiting for I/O to complete.
using (await writeLock.LockAsync())
{
// Send the message
await outputStream.WriteAsync(headerBytes, 0, headerBytes.Length);
await outputStream.WriteAsync(messageBytes, 0, messageBytes.Length);
await outputStream.FlushAsync();
}
}
}
}

View File

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

View File

@@ -0,0 +1,211 @@
//
// 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.Diagnostics;
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.Write(TraceEventType.Information, "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.Write(TraceEventType.Information, "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
}
}

View 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
}
}

View 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.Globalization;
using System.IO;
namespace Microsoft.SqlTools.Hosting.Utility
{
/// <summary>
/// The command-line options helper class.
/// </summary>
public class CommandOptions
{
// set default log directory
// refer to https://jimrich.sk/environment-specialfolder-on-windows-linux-and-os-x/ && https://stackoverflow.com/questions/895723/environment-getfolderpath-commonapplicationdata-is-still-returning-c-docum
// for cross platform locations
internal readonly string DefaultLogRoot = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
/// <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 != null && (arg.StartsWith("--") || arg.StartsWith("-")))
{
// Extracting arguments and properties
arg = arg.Substring(1).ToLowerInvariant();
string argName = arg;
switch (argName)
{
case "-autoflush-log":
AutoFlushLog = true;
break;
case "-tracing-level":
TracingLevel = args[++i];
break;
case "-log-file":
LogFilePath = 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 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 +
" [--autoflush-log] (If passed in auto flushing of log files is enabled., Verbose. Default is to not auto-flush log files)" + Environment.NewLine +
" [--locale **] (default: 'en')" + Environment.NewLine,
" [--log-file **]" + Environment.NewLine +
" [--tracing-level **] (** can be any of: All, Off, Critical, Error, Warning, Information, Verbose. Default is Critical)" + Environment.NewLine +
" [--help]" + Environment.NewLine +
ErrorMessage);
return str;
}
}
public string TracingLevel { get; private set; }
public string LogFilePath { get; private set; }
public bool AutoFlushLog { get; private set; } = false;
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;
}
}
}

View 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);
}
}
}

View File

@@ -0,0 +1,451 @@
//
// 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.Globalization;
using System.IO;
using System.Threading;
namespace Microsoft.SqlTools.Hosting.Utility
{
/// <summary>
/// Ordinal value of each LogEvent value corresponds to a unique event id to be used in trace.
/// By convention explicitly specify the integer value so that when this list grows large it is easy to figure out
/// enumeration corresponding to a numeric value. We could be reserving ranges of values for specific areas or logEvents.
/// Maximum value assignable to LogEvent enum value is 65,535.
/// </summary>
public enum LogEvent : ushort
{
Default = 0,
IoFileSystem = 1,
OsSubSystem = 2,
}
/// <summary>
/// Provides a simple logging interface built on top of .Net tracing frameworks
/// </summary>
public static class Logger
{
internal const SourceLevels defaultTracingLevel = SourceLevels.Critical;
internal const string defaultTraceSource = "sqltools";
private static SourceLevels tracingLevel = defaultTracingLevel;
private static string logFileFullPath;
internal static TraceSource TraceSource { get; set; }
internal static string LogFileFullPath
{
get => logFileFullPath;
private set
{
//If the log file path has a directory component then ensure that the directory exists.
if (!string.IsNullOrEmpty(Path.GetDirectoryName(value)) && !Directory.Exists(Path.GetDirectoryName(value)))
{
Directory.CreateDirectory(Path.GetDirectoryName(value));
}
logFileFullPath = value;
ConfigureListener();
}
}
private static SqlToolsTraceListener Listener { get; set; }
private static void ConfigureLogFile(string logFilePrefix) => LogFileFullPath = GenerateLogFilePath(logFilePrefix);
/// <summary>
/// Calling this method will turn on inclusion CallStack in the log for all future traces
/// </summary>
public static void StartCallStack() => Listener.TraceOutputOptions |= TraceOptions.Callstack;
/// <summary>
/// Calling this method will turn off inclusion of CallStack in the log for all future traces
/// </summary>
public static void StopCallStack() => Listener.TraceOutputOptions &= ~TraceOptions.Callstack;
/// <summary>
/// Calls flush on defaultTracingLevel configured listeners.
/// </summary>
public static void Flush()
{
TraceSource.Flush();
Trace.Flush();
}
public static void Close()
{
Flush();
TraceSource.Close();
Trace.Close();
Listener = null; // Since we have closed the listener, set listener to null.
}
public static SourceLevels TracingLevel
{
get => tracingLevel;
set
{
// configures the source level filter. This alone is not enough for tracing that is done via "Trace" class instead of "TraceSource" object
TraceSource.Switch = new SourceSwitch(TraceSource.Name, value.ToString());
// configure the listener level filter
tracingLevel = value;
Listener.Filter = new EventTypeFilter(tracingLevel);
}
}
public static bool AutoFlush { get; set; } = false;
/// <summary>
/// Initializes the Logger for the current process.
/// </summary>
/// <param name="tracingLevel">
/// Optional. Specifies the minimum log message level to write to the log file.
/// </param>
/// <param name="logFilePath">
/// Optional. Specifies the log name prefix for the log file name at which log messages will be written.
/// <param name="traceSource">
/// Optional. Specifies the tracesource name.
/// </param>
public static void Initialize(
SourceLevels tracingLevel = defaultTracingLevel,
string logFilePath = null,
string traceSource = defaultTraceSource,
bool autoFlush = false)
{
Logger.tracingLevel = tracingLevel;
Logger.AutoFlush = autoFlush;
TraceSource = new TraceSource(traceSource, Logger.tracingLevel);
if (string.IsNullOrWhiteSpace(logFilePath))
{
logFilePath = GenerateLogFilePath(traceSource);
}
LogFileFullPath = logFilePath;
Write(TraceEventType.Information, $"Initialized the {traceSource} logger. Log file is: {LogFileFullPath}");
}
/// <summary>
/// Initializes the Logger for the current process.
/// </summary>
/// </param>
/// <param name="tracingLevel">
/// Optional. Specifies the minimum log message level to write to the log file.
/// </param>
/// <param name="logFilePath">
/// Optional. Specifies the log name prefix for the log file name at which log messages will be written.
/// <param name="traceSource">
/// Optional. Specifies the tracesource name.
/// </param>
public static void Initialize(string tracingLevel, string logFilePath = null, string traceSource = defaultTraceSource)
{
Initialize(Enum.TryParse<SourceLevels>(tracingLevel, out SourceLevels sourceTracingLevel)
? sourceTracingLevel
: defaultTracingLevel
, logFilePath
, traceSource);
}
/// <summary>
/// Configures the LogfilePath for the tracelistener in use for this process.
/// </summary>
/// <returns>
/// Returns the log file path corresponding to logfilePrefix
/// </returns>
public static string GenerateLogFilePath(string logFilePrefix = defaultTraceSource)
{
if (string.IsNullOrWhiteSpace(logFilePrefix))
{
throw new ArgumentOutOfRangeException(nameof(logFilePrefix), $"LogfilePath cannot be configured if argument {nameof(logFilePrefix)} has not been set");
}
// Create the log directory
string logDir = Path.GetDirectoryName(logFilePrefix);
if (!string.IsNullOrWhiteSpace(logDir))
{
if (!Directory.Exists(logDir))
{
try
{
Directory.CreateDirectory(logDir);
}
catch (Exception ex)
{
Write(TraceEventType.Error, LogEvent.IoFileSystem, $"Unable to create directory:{logDir}\nException encountered:{ex}");
}
}
}
// get a unique number to prevent conflicts of two process launching at the same time
int uniqueId;
try
{
uniqueId = Process.GetCurrentProcess().Id;
}
catch (Exception ex)
{
Write(TraceEventType.Information, LogEvent.OsSubSystem, $"Unable to get process id of current running process\nException encountered:{ex}");
// 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
return $"{logFilePrefix}_{DateTime.Now.Year,4:D4}{DateTime.Now.Month,2:D2}{DateTime.Now.Day,2:D2}{DateTime.Now.Hour,2:D2}{DateTime.Now.Minute,2:D2}{DateTime.Now.Second,2:D2}_{uniqueId}.log";
}
private static void ConfigureListener()
{
if (string.IsNullOrWhiteSpace(LogFileFullPath))
{
throw new InvalidOperationException("Listeners cannot be configured if LogFileFullPath has not been set");
}
Listener = new SqlToolsTraceListener(LogFileFullPath)
{
TraceOutputOptions = TraceOptions.DateTime | TraceOptions.ProcessId | TraceOptions.ThreadId,
Filter = new EventTypeFilter(TracingLevel),
};
TraceSource.Listeners.Clear();
TraceSource.Listeners.Add(Listener);
Trace.Listeners.Clear();
Trace.Listeners.Add(Listener);
}
/// <summary>
/// Writes a message to the log file.
/// </summary>
/// <param name="eventType">The level at which the message will be written.</param>
/// <param name="logMessage">The message text to be written.</param>
public static void Write(TraceEventType eventType, string logMessage) => Write(eventType, LogEvent.Default, logMessage);
/// <summary>
/// Writes a message to the log file with accompanying callstack.
/// </summary>
/// <param name="eventType">The level at which the message will be written.</param>
/// <param name="logMessage">The message text to be written.</param>
/// <remarks>
/// The callstack logging gets turned on globally and any other log writes that happens in the time window
/// while this log write is happening will also get callstack information logged. This is not considered
/// and trying to isolate the callstack logging to be turned of for just one call is unnecessarily complex.
/// </remarks>
public static void WriteWithCallstack(TraceEventType eventType, string logMessage) => WriteWithCallstack(eventType, LogEvent.Default, logMessage);
/// <summary>
/// Writes a message to the log file with accompanying callstack.
/// </summary>
/// <param name="eventType">The level at which the message will be written.</param>
/// <param name="logEvent">The event id enumeration for the log event.</param>
/// <param name="logMessage">The message text to be written.</param>
/// <remarks>
/// The callstack logging gets turned on globally and any other log writes that happens in the time window
/// while this log write is happening will also get callstack information logged. This is not considered
/// and trying to isolate the callstack logging to be turned of for just one call is unnecessarily complex.
/// </remarks>
public static void WriteWithCallstack(TraceEventType eventType, LogEvent logEvent, string logMessage)
{
Logger.StartCallStack();
Write(eventType, logEvent, logMessage);
Logger.StopCallStack();
}
/// <summary>
/// Writes a message to the log file.
/// </summary>
/// <param name="eventType">The level at which the message will be written.</param>
/// <param name="logEvent">The event id enumeration for the log event.</param>
/// <param name="logMessage">The message text to be written.</param>
public static void Write(
TraceEventType eventType,
LogEvent logEvent,
string logMessage)
{
// If logger is initialized then use TraceSource else use Trace
if (TraceSource != null)
{
TraceSource.TraceEvent(eventType, (ushort)logEvent, logMessage);
}
else
{
switch (eventType)
{
case TraceEventType.Critical:
case TraceEventType.Error:
if (eventType == TraceEventType.Critical)
{
logMessage = $@"event={eventType}: {logMessage}";
}
Trace.TraceError(logMessage);
break;
case TraceEventType.Warning:
Trace.TraceWarning(logMessage);
break;
case TraceEventType.Information:
case TraceEventType.Resume:
case TraceEventType.Start:
case TraceEventType.Stop:
case TraceEventType.Suspend:
case TraceEventType.Transfer:
case TraceEventType.Verbose:
if (eventType != TraceEventType.Information)
{
logMessage = $@"event={eventType}: {logMessage}";
}
Trace.TraceInformation(logMessage);
break;
}
}
if (AutoFlush)
{
Flush();
}
}
}
/// <summary>
/// This listener has the same behavior as TextWriterTraceListener except it controls how the
/// options: TraceOptions.DateTime, TraceOptions.ProcessId and TraceOptions.ThreadId is written to the output stream.
/// This listener writes the above options, if turned on, inline with the message
/// instead of writing them to indented fields as is the case with TextWriterTraceListener.
/// This implementation also lazily initializes the underlying tracelistener
/// </summary>
/// <remarks>
/// Implementation of this is a lazily initialize trace listener that is partly inspired
/// by: https://stackoverflow.com/questions/30664527/how-to-stop-streamwriter-to-not-to-create-file-if-nothing-to-write
/// </remarks>
internal sealed class SqlToolsTraceListener : TraceListener
{
Lazy<TextWriterTraceListener> _lazyListener;
private TextWriterTraceListener Listener => _lazyListener.Value;
private bool IsListenerCreated => _lazyListener.IsValueCreated;
public SqlToolsTraceListener(string file, string listenerName = "") : base(listenerName)
{
// Wrapping around lazy to make sure that we do not create file if the log.Write events are getting filtered out. i.e. the log file actually gets created the first time an actual write to log file happens.
_lazyListener = new Lazy<TextWriterTraceListener>(
valueFactory: () => new TextWriterTraceListener(new StreamWriter(file, append: true), listenerName),
// LazyThreadSafetyMode.PublicationOnly mode ensures that we keep trying to create the listener (especially the file that write) on all future log.write events even if previous attempt(s) have failed
mode: LazyThreadSafetyMode.PublicationOnly
);
}
#region forward actual write/close/flush/dispose calls to the underlying listener.
public override void Write(string message) => Listener.Write(message);
public override void WriteLine(string message) => Listener.WriteLine(message);
/// <Summary>
/// Closes the <see cref="System.Diagnostics.TextWriterTraceListener.Writer"> so that it no longer
/// receives tracing or debugging output.</see>
/// Make sure that we do not Close if the lazy listener never got created.
/// </Summary>
public override void Close()
{
if (IsListenerCreated)
{
Listener.Close();
}
}
/// <summary>
/// Releases all resources used by the <see cref="SqlToolsTraceListener"/>
/// No unmanaged resources in this class, and it is sealed.
/// No finalizer needed. See http://stackoverflow.com/a/3882819/613130
/// We skip disposing if the lazy listener never got created.
/// </summary>
public new void Dispose()
{
if (IsListenerCreated)
{
Listener.Dispose();
}
}
/// <summary>
/// Flushes the output buffer for the <see cref="System.Diagnostics.TextWriterTraceListener.Writer">.
/// Make sure that we do not Flush if the lazy listener never got created.
/// </summary>
public override void Flush()
{
if (IsListenerCreated)
{
Listener.Flush();
}
}
#endregion
public override void TraceEvent(TraceEventCache eventCache, String source, TraceEventType eventType, int id)
{
TraceEvent(eventCache, source, eventType, id, String.Empty);
}
// All other TraceEvent methods come through this one.
public override void TraceEvent(TraceEventCache eventCache, String source, TraceEventType eventType, int id, string message)
{
if (Filter != null && !Filter.ShouldTrace(eventCache, source, eventType, id, message, null, null, null))
{
return;
}
WriteHeader(eventCache, source, eventType, id);
WriteLine(message);
WriteFooter(eventCache);
}
public override void TraceEvent(TraceEventCache eventCache, String source, TraceEventType eventType, int id, string format, params object[] args)
{
if (Filter != null && !Filter.ShouldTrace(eventCache, source, eventType, id, format, args, null, null))
{
return;
}
WriteHeader(eventCache, source, eventType, id);
if (args != null)
{
WriteLine(String.Format(CultureInfo.InvariantCulture, format, args));
}
else
{
WriteLine(format);
}
WriteFooter(eventCache);
}
private void WriteHeader(TraceEventCache eventCache, String source, TraceEventType eventType, int id)
=> Write(FormatHeader(eventCache, String.Format(CultureInfo.InvariantCulture, "{0} {1}: {2} : ", source, eventType.ToString(), id.ToString(CultureInfo.InvariantCulture))));
private void WriteFooter(TraceEventCache eventCache)
{
if (eventCache == null)
{
return;
}
IndentLevel++;
if (TraceOutputOptions.HasFlag(TraceOptions.LogicalOperationStack))
{
WriteLine("LogicalOperationStack=" + eventCache.LogicalOperationStack);
}
if (TraceOutputOptions.HasFlag(TraceOptions.Callstack))
{
WriteLine("Callstack=" + eventCache.Callstack);
}
IndentLevel--;
}
private string FormatHeader(TraceEventCache eventCache, string message)
{
if (eventCache == null)
{
return message;
}
return $"{(IsEnabled(TraceOptions.DateTime) ? string.Format(CultureInfo.InvariantCulture, "{0} ", eventCache.DateTime.ToLocalTime().ToString("yy-MM-dd H:mm:ss.fffffff", CultureInfo.InvariantCulture)) : string.Empty)}"
+ $"{(IsEnabled(TraceOptions.ProcessId) ? string.Format(CultureInfo.InvariantCulture, "pid:{0} ", eventCache.ProcessId.ToString(CultureInfo.InvariantCulture)) : string.Empty)}"
+ $"{(IsEnabled(TraceOptions.ThreadId) ? string.Format(CultureInfo.InvariantCulture, "tid:{0} ", eventCache.ThreadId.ToString(CultureInfo.InvariantCulture)) : string.Empty)}"
+ message;
}
private bool IsEnabled(TraceOptions opt) => TraceOutputOptions.HasFlag(opt);
}
}

View File

@@ -0,0 +1,17 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using Microsoft.SqlTools.Hosting.v2;
namespace Microsoft.SqlTools.Hosting.Utility
{
public class ServiceProviderNotSetException : InvalidOperationException {
public ServiceProviderNotSetException()
: base(SR.ServiceProviderNotSet) {
}
}
}

View 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.Diagnostics;
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.Write(TraceEventType.Error, $"Exception in exception handling continuation: {e}");
Logger.Write(TraceEventType.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.Write(TraceEventType.Error, $"Exception in exception handling continuation: {e}");
Logger.Write(TraceEventType.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.Write(TraceEventType.Error, sb.ToString());
}
}
}

View 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);
}
}
}
}