Add v2 of the Hosting Service and build nuget packages for it (#675)

* Port v2 of Hosting service to SqlToolsService
- Renamed project to .v2 so that existing hosted service isn't impacted
- Copied over the CoreServices project which contains ConnectionServiceCore and other reusable services for anything interacting with MSSQL
- Ported unit test project across and verified tests run.

* Nuget package support for reusable DLLs

* Use 1.1 version per Karl's suggestion

* Use correct license URL and project URL

* Use new SMO packages
This commit is contained in:
Kevin Cunnane
2018-08-07 12:59:57 -07:00
committed by GitHub
parent 4ac02eab01
commit 71195869e1
224 changed files with 63367 additions and 2 deletions

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,164 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Globalization;
using System.IO;
namespace Microsoft.SqlTools.Hosting.Utility
{
/// <summary>
/// The command-line options helper class.
/// </summary>
public class CommandOptions
{
/// <summary>
/// Construct and parse command line options from the arguments array
/// </summary>
public CommandOptions(string[] args, string serviceName)
{
ServiceName = serviceName;
ErrorMessage = string.Empty;
Locale = string.Empty;
try
{
for (int i = 0; i < args.Length; ++i)
{
string arg = args[i];
if (arg.StartsWith("--") || arg.StartsWith("-"))
{
// Extracting arguments and properties
arg = arg.Substring(1).ToLowerInvariant();
string argName = arg;
switch (argName)
{
case "-enable-logging":
EnableLogging = true;
break;
case "-log-dir":
SetLoggingDirectory(args[++i]);
break;
case "-locale":
SetLocale(args[++i]);
break;
case "h":
case "-help":
ShouldExit = true;
return;
default:
ErrorMessage += string.Format("Unknown argument \"{0}\"" + Environment.NewLine, argName);
break;
}
}
}
}
catch (Exception ex)
{
ErrorMessage += ex.ToString();
return;
}
finally
{
if (!string.IsNullOrEmpty(ErrorMessage) || ShouldExit)
{
Console.WriteLine(Usage);
ShouldExit = true;
}
}
}
/// <summary>
/// Contains any error messages during execution
/// </summary>
public string ErrorMessage { get; private set; }
/// <summary>
/// Whether diagnostic logging is enabled
/// </summary>
public bool EnableLogging { get; private set; }
/// <summary>
/// Gets the directory where log files are output.
/// </summary>
public string LoggingDirectory { get; private set; }
/// <summary>
/// Whether the program should exit immediately. Set to true when the usage is printed.
/// </summary>
public bool ShouldExit { get; private set; }
/// <summary>
/// The locale our we should instantiate this service in
/// </summary>
public string Locale { get; private set; }
/// <summary>
/// Name of service that is receiving command options
/// </summary>
public string ServiceName { get; private set; }
/// <summary>
/// Get the usage string describing command-line arguments for the program
/// </summary>
public string Usage
{
get
{
var str = string.Format("{0}" + Environment.NewLine +
ServiceName + " " + Environment.NewLine +
" Options:" + Environment.NewLine +
" [--enable-logging]" + Environment.NewLine +
" [--log-dir **] (default: current directory)" + Environment.NewLine +
" [--help]" + Environment.NewLine +
" [--locale **] (default: 'en')" + Environment.NewLine,
ErrorMessage);
return str;
}
}
private void SetLoggingDirectory(string loggingDirectory)
{
if (string.IsNullOrWhiteSpace(loggingDirectory))
{
return;
}
this.LoggingDirectory = Path.GetFullPath(loggingDirectory);
}
public virtual void SetLocale(string locale)
{
try
{
LocaleSetter(locale);
}
catch (CultureNotFoundException)
{
// Ignore CultureNotFoundException since it only is thrown before Windows 10. Windows 10,
// along with macOS and Linux, pick up the default culture if an invalid locale is passed
// into the CultureInfo constructor.
}
}
/// <summary>
/// Sets the Locale field used for testing and also sets the global CultureInfo used for
/// culture-specific messages
/// </summary>
/// <param name="locale"></param>
internal void LocaleSetter(string locale)
{
// Creating cultureInfo from our given locale
CultureInfo language = new CultureInfo(locale);
Locale = locale;
// Setting our language globally
CultureInfo.CurrentCulture = language;
CultureInfo.CurrentUICulture = language;
}
}
}

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,299 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
namespace Microsoft.SqlTools.Hosting.Utility
{
/// <summary>
/// Defines the level indicators for log messages.
/// </summary>
public enum LogLevel
{
/// <summary>
/// Indicates a verbose log message.
/// </summary>
Verbose,
/// <summary>
/// Indicates a normal, non-verbose log message.
/// </summary>
Normal,
/// <summary>
/// Indicates a warning message.
/// </summary>
Warning,
/// <summary>
/// Indicates an error message.
/// </summary>
Error
}
/// <summary>
/// Provides a simple logging interface. May be replaced with a
/// more robust solution at a later date.
/// </summary>
public class Logger
{
private static LogWriter logWriter;
private static bool isEnabled;
private static bool isInitialized = false;
private static Lazy<Logger> lazyInstance = new Lazy<Logger>(() => new Logger());
public static Logger Instance
{
get
{
return lazyInstance.Value;
}
}
/// <summary>
/// Initializes the Logger for the current session.
/// </summary>
/// <param name="logFilePath">
/// Optional. Specifies the path at which log messages will be written.
/// </param>
/// <param name="minimumLogLevel">
/// Optional. Specifies the minimum log message level to write to the log file.
/// </param>
public void Initialize(
string logFilePath = "sqltools",
LogLevel minimumLogLevel = LogLevel.Normal,
bool isEnabled = true)
{
Logger.isEnabled = isEnabled;
// return if the logger is not enabled or already initialized
if (!Logger.isEnabled || Logger.isInitialized)
{
return;
}
Logger.isInitialized = true;
// Create the log directory
string logDir = Path.GetDirectoryName(logFilePath);
if (!string.IsNullOrWhiteSpace(logDir))
{
if (!Directory.Exists(logDir))
{
try
{
Directory.CreateDirectory(logDir);
}
catch (Exception)
{
// Creating the log directory is a best effort operation, so ignore any failures.
}
}
}
// get a unique number to prevent conflicts of two process launching at the same time
int uniqueId;
try
{
uniqueId = Process.GetCurrentProcess().Id;
}
catch (Exception)
{
// if the pid look up fails for any reason, just use a random number
uniqueId = new Random().Next(1000, 9999);
}
// make the log path unique
string fullFileName = string.Format(
"{0}_{1,4:D4}{2,2:D2}{3,2:D2}{4,2:D2}{5,2:D2}{6,2:D2}{7}.log",
logFilePath,
DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day,
DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second,
uniqueId);
if (logWriter != null)
{
logWriter.Dispose();
}
// TODO: Parameterize this
logWriter =
new LogWriter(
minimumLogLevel,
fullFileName,
true);
Write(LogLevel.Normal, "Initializing SQL Tools Service Host logger");
}
/// <summary>
/// Closes the Logger.
/// </summary>
public void Close()
{
if (logWriter != null)
{
logWriter.Dispose();
}
}
/// <summary>
/// Writes a message to the log file.
/// </summary>
/// <param name="logLevel">The level at which the message will be written.</param>
/// <param name="logMessage">The message text to be written.</param>
/// <param name="callerName">The name of the calling method.</param>
/// <param name="callerSourceFile">The source file path where the calling method exists.</param>
/// <param name="callerLineNumber">The line number of the calling method.</param>
public void Write(
LogLevel logLevel,
string logMessage,
[CallerMemberName] string callerName = null,
[CallerFilePath] string callerSourceFile = null,
[CallerLineNumber] int callerLineNumber = 0)
{
// return if the logger is not enabled or not initialized
if (!Logger.isEnabled || !Logger.isInitialized)
{
return;
}
if (logWriter != null)
{
logWriter.Write(
logLevel,
logMessage,
callerName,
callerSourceFile,
callerLineNumber);
}
}
}
internal class LogWriter : IDisposable
{
private object logLock = new object();
private TextWriter textWriter;
private LogLevel minimumLogLevel = LogLevel.Verbose;
public LogWriter(LogLevel minimumLogLevel, string logFilePath, bool deleteExisting)
{
this.minimumLogLevel = minimumLogLevel;
// Ensure that we have a usable log file path
if (!Path.IsPathRooted(logFilePath))
{
logFilePath =
Path.Combine(
AppContext.BaseDirectory,
logFilePath);
}
if (!this.TryOpenLogFile(logFilePath, deleteExisting))
{
// If the log file couldn't be opened at this location,
// try opening it in a more reliable path
this.TryOpenLogFile(
Path.Combine(
Environment.GetEnvironmentVariable("TEMP"),
Path.GetFileName(logFilePath)),
deleteExisting);
}
}
public void Write(
LogLevel logLevel,
string logMessage,
string callerName = null,
string callerSourceFile = null,
int callerLineNumber = 0)
{
if (this.textWriter != null &&
logLevel >= this.minimumLogLevel)
{
// System.IO is not thread safe
lock (this.logLock)
{
// Print the timestamp and log level
this.textWriter.WriteLine(
"{0} [{1}] - Method \"{2}\" at line {3} of {4}\r\n",
DateTime.Now,
logLevel.ToString().ToUpper(),
callerName,
callerLineNumber,
callerSourceFile);
// Print out indented message lines
foreach (var messageLine in logMessage.Split('\n'))
{
this.textWriter.WriteLine(" " + messageLine.TrimEnd());
}
// Finish with a newline and flush the writer
this.textWriter.WriteLine();
this.textWriter.Flush();
}
}
}
public void Dispose()
{
if (this.textWriter != null)
{
this.textWriter.Flush();
this.textWriter.Dispose();
this.textWriter = null;
}
}
private bool TryOpenLogFile(
string logFilePath,
bool deleteExisting)
{
try
{
// Make sure the log directory exists
Directory.CreateDirectory(
Path.GetDirectoryName(
logFilePath));
// Open the log file for writing with UTF8 encoding
this.textWriter =
new StreamWriter(
new FileStream(
logFilePath,
deleteExisting ?
FileMode.Create :
FileMode.Append),
Encoding.UTF8);
return true;
}
catch (Exception e)
{
if (e is UnauthorizedAccessException ||
e is IOException)
{
// This exception is thrown when we can't open the file
// at the path in logFilePath. Return false to indicate
// that the log file couldn't be created.
return false;
}
// Unexpected exception, rethrow it
throw;
}
}
}
}

View File

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

View File

@@ -0,0 +1,99 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.SqlTools.Hosting.Utility
{
public static class TaskExtensions
{
/// <summary>
/// Adds handling to check the Exception field of a task and log it if the task faulted
/// </summary>
/// <remarks>
/// This will effectively swallow exceptions in the task chain.
/// </remarks>
/// <param name="antecedent">The task to continue</param>
/// <param name="continuationAction">
/// An optional operation to perform after exception handling has occurred
/// </param>
/// <returns>Task with exception handling on continuation</returns>
public static Task ContinueWithOnFaulted(this Task antecedent, Action<Task> continuationAction)
{
return antecedent.ContinueWith(task =>
{
// If the task hasn't faulted or has an exception, skip processing
if (!task.IsFaulted || task.Exception == null)
{
return;
}
LogTaskExceptions(task.Exception);
// Run the continuation task that was provided
try
{
continuationAction?.Invoke(task);
}
catch (Exception e)
{
Logger.Instance.Write(LogLevel.Error, $"Exception in exception handling continuation: {e}");
Logger.Instance.Write(LogLevel.Error, e.StackTrace);
}
});
}
/// <summary>
/// Adds handling to check the Exception field of a task and log it if the task faulted.
/// This version allows for async code to be ran in the continuation function.
/// </summary>
/// <remarks>
/// This will effectively swallow exceptions in the task chain.
/// </remarks>
/// <param name="antecedent">The task to continue</param>
/// <param name="continuationFunc">
/// An optional operation to perform after exception handling has occurred
/// </param>
/// <returns>Task with exception handling on continuation</returns>
public static Task ContinueWithOnFaulted(this Task antecedent, Func<Task, Task> continuationFunc)
{
return antecedent.ContinueWith(task =>
{
// If the task hasn't faulted or doesn't have an exception, skip processing
if (!task.IsFaulted || task.Exception == null)
{
return;
}
LogTaskExceptions(task.Exception);
// Run the continuation task that was provided
try
{
continuationFunc?.Invoke(antecedent).Wait();
}
catch (Exception e)
{
Logger.Instance.Write(LogLevel.Error, $"Exception in exception handling continuation: {e}");
Logger.Instance.Write(LogLevel.Error, e.StackTrace);
}
});
}
private static void LogTaskExceptions(AggregateException exception)
{
// Construct an error message for an aggregate exception and log it
StringBuilder sb = new StringBuilder("Unhandled exception(s) in async task:");
foreach (Exception e in exception.InnerExceptions)
{
sb.AppendLine($"{e.GetType().Name}: {e.Message}");
sb.AppendLine(e.StackTrace);
}
Logger.Instance.Write(LogLevel.Error, sb.ToString());
}
}
}

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