//
// 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.Linq;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Assessment;
using Microsoft.SqlServer.Management.Assessment.Configuration;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection.ReliableConnection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts;
using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.TaskServices;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.Utility;
using AssessmentResultItem = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.AssessmentResultItem;
using ConnectionType = Microsoft.SqlTools.ServiceLayer.Connection.ConnectionType;
using InvokeParams = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.InvokeParams;
using InvokeRequest = Microsoft.SqlTools.ServiceLayer.SqlAssessment.Contracts.InvokeRequest;
namespace Microsoft.SqlTools.ServiceLayer.SqlAssessment
{
///
/// Service for running SQL Assessment.
///
public sealed class SqlAssessmentService : IDisposable
{
private const string ApiVersion = "1.0";
#region Singleton Instance Implementation
private static readonly Lazy LazyInstance
= new Lazy(() => new SqlAssessmentService());
internal SqlAssessmentService(
ConnectionService connService,
WorkspaceService workspaceService)
{
ConnectionService = connService;
WorkspaceService = workspaceService;
}
private SqlAssessmentService()
{
ConnectionService = ConnectionService.Instance;
WorkspaceService = WorkspaceService.Instance;
}
///
/// Singleton instance of the query execution service
///
public static SqlAssessmentService Instance => LazyInstance.Value;
#endregion
#region Properties
///
/// Gets the used to run assessment operations.
///
internal Engine Engine { get; } = new Engine();
///
/// Gets the instance of the connection service,
/// used to get the connection info for a given owner URI.
///
private ConnectionService ConnectionService { get; }
private WorkspaceService WorkspaceService { get; }
///
/// Holds a map from the
/// to a that is being ran.
///
private readonly Lazy> activeRequests =
new Lazy>(() => new ConcurrentDictionary());
///
/// Gets a map from the
/// to a that is being ran.
///
internal ConcurrentDictionary ActiveRequests => activeRequests.Value;
#endregion
///
/// Initializes the service with the service host,
/// registers request handlers and shutdown event handler.
///
/// The service host instance to register with
public void InitializeService(ServiceHost serviceHost)
{
// Register handlers for requests
serviceHost.SetRequestHandler(InvokeRequest.Type, HandleInvokeRequest);
serviceHost.SetRequestHandler(GetAssessmentItemsRequest.Type, HandleGetAssessmentItemsRequest);
serviceHost.SetRequestHandler(GenerateScriptRequest.Type, HandleGenerateScriptRequest);
// Register handler for shutdown event
serviceHost.RegisterShutdownTask((shutdownParams, requestContext) =>
{
Dispose();
return Task.FromResult(0);
});
}
#region Request Handlers
internal Task HandleGetAssessmentItemsRequest(
GetAssessmentItemsParams itemsParams,
RequestContext> requestContext)
{
return this.HandleAssessmentRequest(requestContext, itemsParams, this.GetAssessmentItems);
}
internal Task HandleInvokeRequest(
InvokeParams invokeParams,
RequestContext> requestContext)
{
return this.HandleAssessmentRequest(requestContext, invokeParams, this.InvokeSqlAssessment);
}
internal async Task HandleGenerateScriptRequest(
GenerateScriptParams parameters,
RequestContext requestContext)
{
GenerateScriptOperation operation = null;
try
{
operation = new GenerateScriptOperation(parameters);
TaskMetadata metadata = new TaskMetadata
{
TaskOperation = operation,
TaskExecutionMode = parameters.TaskExecutionMode,
ServerName = parameters.TargetServerName,
DatabaseName = parameters.TargetDatabaseName,
Name = SR.SqlAssessmentGenerateScriptTaskName
};
var _ = SqlTaskManager.Instance.CreateAndRun(metadata);
await requestContext.SendResult(new ResultStatus
{
Success = true,
ErrorMessage = operation.ErrorMessage
});
}
catch (Exception e)
{
Logger.Write(TraceEventType.Error, "SQL Assessment: failed to generate a script. Error: " + e);
await requestContext.SendResult(new ResultStatus()
{
Success = false,
ErrorMessage = operation == null ? e.Message : operation.ErrorMessage,
});
}
}
#endregion
#region Helpers
private async Task HandleAssessmentRequest(
RequestContext> requestContext,
AssessmentParams requestParams,
Func>> assessmentFunction)
where TResult : AssessmentItemInfo
{
try
{
string randomUri = Guid.NewGuid().ToString();
// get connection
if (!ConnectionService.TryFindConnection(requestParams.OwnerUri, out var connInfo))
{
await requestContext.SendError(SR.SqlAssessmentQueryInvalidOwnerUri);
return;
}
ConnectParams connectParams = new ConnectParams
{
OwnerUri = randomUri,
Connection = connInfo.ConnectionDetails,
Type = ConnectionType.Default
};
if(!connInfo.TryGetConnection(ConnectionType.Default, out var connection))
{
await requestContext.SendError(SR.SqlAssessmentConnectingError);
}
var workTask = CallAssessmentEngine(
requestParams,
connectParams,
randomUri,
assessmentFunction)
.ContinueWith(async tsk =>
{
await requestContext.SendResult(tsk.Result);
});
ActiveRequests.TryAdd(randomUri, workTask);
}
catch (Exception ex)
{
if (ex is StackOverflowException || ex is OutOfMemoryException)
{
throw;
}
await requestContext.SendError(ex.ToString());
}
}
///
/// This function obtains a live connection, then calls
/// an assessment operation specified by
///
///
/// SQL Assessment result item type.
///
///
/// Request parameters passed from the host.
///
///
/// Connection parameters used to identify and access the target.
///
///
/// An URI identifying the request task to enable concurrent execution.
///
///
/// A function performing assessment operation for given target.
///
///
/// Returns for given target.
///
internal async Task> CallAssessmentEngine(
AssessmentParams requestParams,
ConnectParams connectParams,
string taskUri,
Func>> assessmentFunc)
where TResult : AssessmentItemInfo
{
var result = new AssessmentResult
{
ApiVersion = ApiVersion
};
await ConnectionService.Connect(connectParams);
var connection = await ConnectionService.Instance.GetOrOpenConnection(taskUri, ConnectionType.Query);
try
{
var serverInfo = ReliableConnectionHelper.GetServerVersion(connection);
var hostInfo = ReliableConnectionHelper.GetServerHostInfo(connection);
var server = new SqlObjectLocator
{
Connection = connection,
EngineEdition = GetEngineEdition(serverInfo.EngineEditionId),
Name = serverInfo.ServerName,
ServerName = serverInfo.ServerName,
Type = SqlObjectType.Server,
Urn = serverInfo.ServerName,
Version = Version.Parse(serverInfo.ServerVersion),
Platform = hostInfo.Platform
};
switch (requestParams.TargetType)
{
case SqlObjectType.Server:
Logger.Write(
TraceEventType.Verbose,
$"SQL Assessment: running an operation on a server, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
result.Items.AddRange(await assessmentFunc(server));
Logger.Write(
TraceEventType.Verbose,
$"SQL Assessment: finished an operation on a server, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
break;
case SqlObjectType.Database:
var db = GetDatabaseLocator(server, connection.Database);
Logger.Write(
TraceEventType.Verbose,
$"SQL Assessment: running an operation on a database, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
result.Items.AddRange(await assessmentFunc(db));
Logger.Write(
TraceEventType.Verbose,
$"SQL Assessment: finished an operation on a database, platform:{server.Platform}, edition:{server.EngineEdition.ToString()}, version:{server.Version}");
break;
}
result.Success = true;
}
finally
{
ActiveRequests.TryRemove(taskUri, out _);
ConnectionService.Disconnect(new DisconnectParams { OwnerUri = taskUri, Type = null });
}
return result;
}
///
/// Invokes SQL Assessment and formats results.
///
///
/// A sequence of target servers or databases to be assessed.
///
///
/// Returns a
/// containing assessment results.
///
///
/// Internal for testing
///
internal async Task> InvokeSqlAssessment(SqlObjectLocator target)
{
var resultsList = await Engine.GetAssessmentResultsList(target);
Logger.Write(TraceEventType.Verbose, $"SQL Assessment: got {resultsList.Count} results.");
return resultsList.Select(TranslateAssessmentResult).ToList();
}
///
/// Gets the list of checks for given target servers or databases.
///
///
/// A sequence of target servers or databases.
///
///
/// Returns an
/// containing checks available for given .
///
internal Task> GetAssessmentItems(SqlObjectLocator target)
{
var result = new List();
var resultsList = Engine.GetChecks(target).ToList();
Logger.Write(TraceEventType.Verbose, $"SQL Assessment: got {resultsList.Count} items.");
foreach (var r in resultsList)
{
var targetName = target.Type != SqlObjectType.Server
? $"{target.ServerName}:{target.Name}"
: target.Name;
var item = new CheckInfo()
{
CheckId = r.Id,
Description = r.Description,
DisplayName = r.DisplayName,
HelpLink = r.HelpLink,
Level = r.Level.ToString(),
TargetName = targetName,
Tags = r.Tags.ToArray(),
TargetType = target.Type,
RulesetName = Engine.Configuration.DefaultRuleset.Name,
RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString()
};
result.Add(item);
}
return Task.FromResult(result);
}
private AssessmentResultItem TranslateAssessmentResult(IAssessmentResult r)
{
var item = new AssessmentResultItem
{
CheckId = r.Check.Id,
Description = r.Check.Description,
DisplayName = r.Check.DisplayName,
HelpLink = r.Check.HelpLink,
Level = r.Check.Level.ToString(),
Message = r.Message,
TargetName = r.TargetPath,
Tags = r.Check.Tags.ToArray(),
TargetType = r.TargetType,
RulesetVersion = Engine.Configuration.DefaultRuleset.Version.ToString(),
RulesetName = Engine.Configuration.DefaultRuleset.Name,
Timestamp = r.Timestamp
};
if (r is IAssessmentNote)
{
item.Kind = AssessmentResultItemKind.Note;
}
else if (r is IAssessmentWarning)
{
item.Kind = AssessmentResultItemKind.Warning;
}
else if (r is IAssessmentError)
{
item.Kind = AssessmentResultItemKind.Error;
}
return item;
}
///
/// Constructs a for specified database.
///
/// Target server locator.
/// Target database name.
/// Returns a locator for target database.
private static SqlObjectLocator GetDatabaseLocator(SqlObjectLocator server, string databaseName)
{
return new SqlObjectLocator
{
Connection = server.Connection,
EngineEdition = server.EngineEdition,
Name = databaseName,
Platform = server.Platform,
ServerName = server.Name,
Type = SqlObjectType.Database,
Urn = $"{server.Name}:{databaseName}",
Version = server.Version
};
}
///
/// Converts numeric of engine edition
/// returned by SERVERPROPERTY('EngineEdition').
///
///
/// A number returned by SERVERPROPERTY('EngineEdition').
///
/// Engine edition is not supported.
///
/// Returns a
/// corresponding to the .
///
private static SqlEngineEdition GetEngineEdition(int representation)
{
switch (representation)
{
case 1: return SqlEngineEdition.PersonalOrDesktopEngine;
case 2: return SqlEngineEdition.Standard;
case 3: return SqlEngineEdition.Enterprise;
case 4: return SqlEngineEdition.Express;
case 5: return SqlEngineEdition.AzureDatabase;
case 6: return SqlEngineEdition.DataWarehouse;
case 7: return SqlEngineEdition.StretchDatabase;
case 8: return SqlEngineEdition.ManagedInstance;
default:
throw new ArgumentOutOfRangeException(nameof(representation),
SR.SqlAssessmentUnsuppoertedEdition(representation));
}
}
#endregion
#region IDisposable Implementation
private bool disposed;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
foreach (var request in ActiveRequests)
{
request.Value.Dispose();
}
ActiveRequests.Clear();
}
disposed = true;
}
~SqlAssessmentService()
{
Dispose(false);
}
#endregion
}
}