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