diff --git a/bin/nuget/Microsoft.SqlServer.Management.SqlScriptPublishModel.140.2.4.nupkg b/bin/nuget/Microsoft.SqlServer.Management.SqlScriptPublishModel.140.2.4.nupkg deleted file mode 100644 index 059a6577..00000000 Binary files a/bin/nuget/Microsoft.SqlServer.Management.SqlScriptPublishModel.140.2.4.nupkg and /dev/null differ diff --git a/bin/nuget/Microsoft.SqlServer.Smo.140.2.4.nupkg b/bin/nuget/Microsoft.SqlServer.Smo.140.2.4.nupkg deleted file mode 100644 index 6f11b8bd..00000000 Binary files a/bin/nuget/Microsoft.SqlServer.Smo.140.2.4.nupkg and /dev/null differ diff --git a/bin/nuget/Microsoft.SqlServer.Smo.140.2.5.nupkg b/bin/nuget/Microsoft.SqlServer.Smo.140.2.5.nupkg new file mode 100644 index 00000000..17c0e698 Binary files /dev/null and b/bin/nuget/Microsoft.SqlServer.Smo.140.2.5.nupkg differ diff --git a/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj b/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj index 88bd1037..01bc420c 100644 --- a/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj +++ b/src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj @@ -19,8 +19,7 @@ - - + diff --git a/src/Microsoft.SqlTools.Hosting/Microsoft.SqlTools.Hosting.csproj b/src/Microsoft.SqlTools.Hosting/Microsoft.SqlTools.Hosting.csproj index 8cc08212..1d90eb48 100644 --- a/src/Microsoft.SqlTools.Hosting/Microsoft.SqlTools.Hosting.csproj +++ b/src/Microsoft.SqlTools.Hosting/Microsoft.SqlTools.Hosting.csproj @@ -14,8 +14,7 @@ - - + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs index 5583a837..cdad9296 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/ConnectionService.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; using System.Data.SqlClient; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -1086,5 +1087,40 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection } } } + + /// + /// Create and open a new SqlConnection from a ConnectionInfo object + /// Note: we need to audit all uses of this method to determine why we're + /// bypassing normal ConnectionService connection management + /// + internal static SqlConnection OpenSqlConnection(ConnectionInfo connInfo) + { + try + { + // increase the connection timeout to at least 30 seconds and and build connection string + // enable PersistSecurityInfo to handle issues in SMO where the connection context is lost in reconnections + int? originalTimeout = connInfo.ConnectionDetails.ConnectTimeout; + bool? originalPersistSecurityInfo = connInfo.ConnectionDetails.PersistSecurityInfo; + connInfo.ConnectionDetails.ConnectTimeout = Math.Max(30, originalTimeout ?? 0); + connInfo.ConnectionDetails.PersistSecurityInfo = true; + string connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); + connInfo.ConnectionDetails.ConnectTimeout = originalTimeout; + connInfo.ConnectionDetails.PersistSecurityInfo = originalPersistSecurityInfo; + + // open a dedicated binding server connection + SqlConnection sqlConn = new SqlConnection(connectionString); + sqlConn.Open(); + return sqlConn; + } + catch (Exception ex) + { + string error = string.Format(CultureInfo.InvariantCulture, + "Failed opening a SqlConnection: error:{0} inner:{1} stacktrace:{2}", + ex.Message, ex.InnerException != null ? ex.InnerException.Message : string.Empty, ex.StackTrace); + Logger.Write(LogLevel.Error, error); + } + + return null; + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionDetails.cs b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionDetails.cs index 707ad49f..1cf7bf92 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionDetails.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Connection/Contracts/ConnectionDetails.cs @@ -22,8 +22,8 @@ namespace Microsoft.SqlTools.ServiceLayer.Connection.Contracts /// /// Gets or sets the connection password /// - /// - public string Password { + public string Password + { get { return GetOptionValue("password"); diff --git a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs index 4bae7958..0e914b9f 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/DisasterRecovery/DisasterRecoveryService.cs @@ -143,8 +143,8 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery if (connInfo != null) { DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(connInfo, databaseExists: true); - SqlConnection sqlConn = GetSqlConnection(connInfo); - if ((sqlConn != null) && !connInfo.IsSqlDW && !connInfo.IsAzure) + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo); + if (sqlConn != null && !connInfo.IsSqlDW && !connInfo.IsAzure) { BackupConfigInfo backupConfigInfo = this.GetBackupConfigInfo(helper.DataContainer, sqlConn, sqlConn.Database); backupConfigInfo.DatabaseInfo = AdminService.GetDatabaseInfo(connInfo); @@ -296,7 +296,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery if (supported && connInfo != null) { DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(connInfo, databaseExists: true); - SqlConnection sqlConn = GetSqlConnection(connInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo); BackupOperation backupOperation = CreateBackupOperation(helper.DataContainer, sqlConn, backupParams.BackupInfo); SqlTask sqlTask = null; @@ -320,32 +320,6 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery } } - internal static SqlConnection GetSqlConnection(ConnectionInfo connInfo) - { - try - { - // increase the connection timeout to at least 30 seconds and and build connection string - // enable PersistSecurityInfo to handle issues in SMO where the connection context is lost in reconnections - int? originalTimeout = connInfo.ConnectionDetails.ConnectTimeout; - bool? originalPersistSecurityInfo = connInfo.ConnectionDetails.PersistSecurityInfo; - connInfo.ConnectionDetails.ConnectTimeout = Math.Max(30, originalTimeout ?? 0); - connInfo.ConnectionDetails.PersistSecurityInfo = true; - string connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); - connInfo.ConnectionDetails.ConnectTimeout = originalTimeout; - connInfo.ConnectionDetails.PersistSecurityInfo = originalPersistSecurityInfo; - - // open a dedicated binding server connection - SqlConnection sqlConn = new SqlConnection(connectionString); - sqlConn.Open(); - return sqlConn; - } - catch (Exception) - { - } - - return null; - } - private bool IsBackupRestoreOperationSupported(string ownerUri, out ConnectionInfo connectionInfo) { SqlConnection sqlConn = null; @@ -358,8 +332,8 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery if (connInfo != null) { - sqlConn = GetSqlConnection(connInfo); - if ((sqlConn != null) && !connInfo.IsSqlDW && !connInfo.IsAzure) + sqlConn = ConnectionService.OpenSqlConnection(connInfo); + if (sqlConn != null && !connInfo.IsSqlDW && !connInfo.IsAzure) { connectionInfo = connInfo; return true; diff --git a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserService.cs b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserService.cs index 468d0b27..d914db17 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/FileBrowser/FileBrowserService.cs @@ -114,7 +114,7 @@ namespace Microsoft.SqlTools.ServiceLayer.FileBrowser { try { - Task.Run(() => RunFileBrowserOpenTask(fileBrowserParams)); + var task = Task.Run(() => RunFileBrowserOpenTask(fileBrowserParams)); await requestContext.SendResult(true); } catch @@ -129,7 +129,7 @@ namespace Microsoft.SqlTools.ServiceLayer.FileBrowser { try { - Task.Run(() => RunFileBrowserExpandTask(fileBrowserParams)); + var task = Task.Run(() => RunFileBrowserExpandTask(fileBrowserParams)); await requestContext.SendResult(true); } catch @@ -144,7 +144,7 @@ namespace Microsoft.SqlTools.ServiceLayer.FileBrowser { try { - Task.Run(() => RunFileBrowserValidateTask(fileBrowserParams)); + var task = Task.Run(() => RunFileBrowserValidateTask(fileBrowserParams)); await requestContext.SendResult(true); } catch diff --git a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs index 6a1bbdbd..945f2b80 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/HostLoader.cs @@ -15,6 +15,7 @@ using Microsoft.SqlTools.ServiceLayer.EditData; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.LanguageServices; using Microsoft.SqlTools.ServiceLayer.Metadata; +using Microsoft.SqlTools.ServiceLayer.Profiler; using Microsoft.SqlTools.ServiceLayer.QueryExecution; using Microsoft.SqlTools.ServiceLayer.Scripting; using Microsoft.SqlTools.ServiceLayer.SqlContext; @@ -94,6 +95,9 @@ namespace Microsoft.SqlTools.ServiceLayer DisasterRecoveryService.Instance.InitializeService(serviceHost); serviceProvider.RegisterSingleService(DisasterRecoveryService.Instance); + ProfilerService.Instance.InitializeService(serviceHost); + serviceProvider.RegisterSingleService(ProfilerService.Instance); + InitializeHostedServices(serviceProvider, serviceHost); serviceHost.ServiceProvider = serviceProvider; diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs index a74810d5..a4645787 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs @@ -81,20 +81,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices try { bindingContext.BindingLock.Reset(); - - // increase the connection timeout to at least 30 seconds and and build connection string - // enable PersistSecurityInfo to handle issues in SMO where the connection context is lost in reconnections - int? originalTimeout = connInfo.ConnectionDetails.ConnectTimeout; - bool? originalPersistSecurityInfo = connInfo.ConnectionDetails.PersistSecurityInfo; - connInfo.ConnectionDetails.ConnectTimeout = Math.Max(DefaultMinimumConnectionTimeout, originalTimeout ?? 0); - connInfo.ConnectionDetails.PersistSecurityInfo = true; - string connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); - connInfo.ConnectionDetails.ConnectTimeout = originalTimeout; - connInfo.ConnectionDetails.PersistSecurityInfo = originalPersistSecurityInfo; - - // open a dedicated binding server connection - SqlConnection sqlConn = new SqlConnection(connectionString); - sqlConn.Open(); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo); // populate the binding context to work with the SMO metadata provider ServerConnection serverConn = new ServerConnection(sqlConn); diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs index 26c23f71..32352490 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.cs @@ -3517,6 +3517,14 @@ namespace Microsoft.SqlTools.ServiceLayer } } + public static string ProfilerConnectionNotFound + { + get + { + return Keys.GetString(Keys.ProfilerConnectionNotFound); + } + } + public static string ConnectionServiceListDbErrorNotConnected(string uri) { return Keys.GetString(Keys.ConnectionServiceListDbErrorNotConnected, uri); @@ -4945,6 +4953,9 @@ namespace Microsoft.SqlTools.ServiceLayer public const string InvalidPathError = "InvalidPathError"; + public const string ProfilerConnectionNotFound = "ProfilerConnectionNotFound"; + + private Keys() { } diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx index 0f97e781..845c54cb 100755 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.resx @@ -1931,4 +1931,8 @@ Cannot access the specified path on the server: {0} + + Connection not found + + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings index 3515ef10..ee8e9a66 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.strings @@ -848,4 +848,8 @@ ScriptTaskName = scripting ############################################################################ # File Browser Validation Errors -InvalidPathError = Cannot access the specified path on the server: {0} \ No newline at end of file +InvalidPathError = Cannot access the specified path on the server: {0} + +############################################################################ +# Profiler +ProfilerConnectionNotFound = Connection not found diff --git a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf index f4312ad7..c5d72990 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf +++ b/src/Microsoft.SqlTools.ServiceLayer/Localization/sr.xlf @@ -2250,6 +2250,11 @@ scripting + + Connection not found + Connection not found + + The provided path specifies a directory but a file path is required: {0} The file name specified is also a directory name: {0} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs b/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs index e5ec100f..d2a75605 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Metadata/MetadataService.cs @@ -74,7 +74,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Metadata var metadata = new List(); if (connInfo != null) { - using (SqlConnection sqlConn = OpenMetadataConnection(connInfo)) + using (SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo)) { ReadMetadata(sqlConn, metadata); } @@ -129,7 +129,7 @@ namespace Microsoft.SqlTools.ServiceLayer.Metadata ColumnMetadata[] metadata = null; if (connInfo != null) { - SqlConnection sqlConn = OpenMetadataConnection(connInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(connInfo); TableMetadata table = new SmoMetadataFactory().GetObjectMetadata( sqlConn, metadataParams.Schema, metadataParams.ObjectName, objectType); @@ -147,35 +147,6 @@ namespace Microsoft.SqlTools.ServiceLayer.Metadata } } - /// - /// Create a SqlConnection to use for querying metadata - /// - internal static SqlConnection OpenMetadataConnection(ConnectionInfo connInfo) - { - try - { - // increase the connection timeout to at least 30 seconds and and build connection string - // enable PersistSecurityInfo to handle issues in SMO where the connection context is lost in reconnections - int? originalTimeout = connInfo.ConnectionDetails.ConnectTimeout; - bool? originalPersistSecurityInfo = connInfo.ConnectionDetails.PersistSecurityInfo; - connInfo.ConnectionDetails.ConnectTimeout = Math.Max(30, originalTimeout ?? 0); - connInfo.ConnectionDetails.PersistSecurityInfo = true; - string connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); - connInfo.ConnectionDetails.ConnectTimeout = originalTimeout; - connInfo.ConnectionDetails.PersistSecurityInfo = originalPersistSecurityInfo; - - // open a dedicated binding server connection - SqlConnection sqlConn = new SqlConnection(connectionString); - sqlConn.Open(); - return sqlConn; - } - catch (Exception) - { - } - - return null; - } - internal static bool IsSystemDatabase(string database) { // compare against master for now diff --git a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj index abbd2aca..71a2bb4b 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj +++ b/src/Microsoft.SqlTools.ServiceLayer/Microsoft.SqlTools.ServiceLayer.csproj @@ -19,8 +19,7 @@ - - + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/ProfilerEvent.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/ProfilerEvent.cs new file mode 100644 index 00000000..a925d126 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/ProfilerEvent.cs @@ -0,0 +1,78 @@ +// +// 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; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler.Contracts +{ + /// + /// Class that contains data for a single profile event + /// + public class ProfilerEvent + { + /// + /// Initialize a new ProfilerEvent with required parameters + /// + public ProfilerEvent(string name, string timestamp) + { + this.Name = name; + this.Timestamp = timestamp; + this.Values = new Dictionary(); + } + + /// + /// Profiler event name + /// + public string Name { get; private set; } + + /// + /// Profiler event timestamp + /// + public string Timestamp { get; private set; } + + /// + /// Profiler event values collection + /// + public Dictionary Values { get; private set; } + + /// + /// Equals method + /// + public bool Equals(ProfilerEvent p) + { + // if parameter is null return false: + if ((object)p == null) + { + return false; + } + + return this.Name == p.Name + && this.Timestamp == p.Timestamp + && this.Values.Count == p.Values.Count; + } + + /// + /// GetHashCode method + /// + public override int GetHashCode() + { + int hashCode = this.GetType().ToString().GetHashCode(); + + if (this.Name != null) + { + hashCode ^= this.Name.GetHashCode(); + } + + if (this.Timestamp != null) + { + hashCode ^= this.Timestamp.GetHashCode(); + } + + hashCode ^= this.Values.Count.GetHashCode(); + + return hashCode; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/ProfilerEventsAvailableNotification.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/ProfilerEventsAvailableNotification.cs new file mode 100644 index 00000000..161e5766 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/ProfilerEventsAvailableNotification.cs @@ -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.Collections.Generic; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler.Contracts +{ + public class ProfilerEventsAvailableParams + { + public string SessionId { get; set; } + + public List Events { get; set; } + } + + public class ProfilerEventsAvailableNotification + { + public static readonly + EventType Type = + EventType.Create("profiler/eventsavailable"); + } +} + + diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/StartProfilingRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/StartProfilingRequest.cs new file mode 100644 index 00000000..26de7c3d --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/StartProfilingRequest.cs @@ -0,0 +1,55 @@ +// +// 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.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler.Contracts +{ + /// + /// Start Profiling request parameters + /// + public class StartProfilingParams : GeneralRequestDetails + { + public string OwnerUri { get; set; } + + public string TemplateName + { + get + { + return GetOptionValue("templateName"); + } + set + { + SetOptionValue("templateName", value); + } + } + } + + public class StartProfilingResult + { + /// + /// Session ID that was started + /// + public string SessionId { get; set; } + + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// Start Profile request type + /// + public class StartProfilingRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("profiler/start"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/StopProfilingRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/StopProfilingRequest.cs new file mode 100644 index 00000000..45428259 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/Contracts/StopProfilingRequest.cs @@ -0,0 +1,38 @@ +// +// 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.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler.Contracts +{ + /// + /// Stop Profiling request parameters + /// + public class StopProfilingParams + { + public string SessionId { get; set; } + } + + public class StopProfilingResult + { + public bool Succeeded { get; set; } + + public string ErrorMessage { get; set; } + } + + /// + /// Start Profile request type + /// + public class StopProfilingRequest + { + /// + /// Request definition + /// + public static readonly + RequestType Type = + RequestType.Create("profiler/stop"); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/IProfilerSessionListener.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IProfilerSessionListener.cs new file mode 100644 index 00000000..846f3a36 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IProfilerSessionListener.cs @@ -0,0 +1,15 @@ +// +// 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.ServiceLayer.Profiler.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler +{ + public interface IProfilerSessionListener + { + void EventsAvailable(string sessionId, List events); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/IProfilerSessionMonitor.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IProfilerSessionMonitor.cs new file mode 100644 index 00000000..56117e37 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IProfilerSessionMonitor.cs @@ -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; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Linq; +using System.Xml; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.XEvent; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler +{ + /// + /// Profiler session monitor interface + /// + public interface IProfilerSessionMonitor + { + /// + /// Starts monitoring a profiler session + /// + bool StartMonitoringSession(ProfilerSession session); + + /// + /// Stops monitoring a profiler session + /// + bool StopMonitoringSession(string sessionId); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/IXEventSession.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IXEventSession.cs new file mode 100644 index 00000000..959a5d07 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IXEventSession.cs @@ -0,0 +1,18 @@ +// +// 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.ServiceLayer.Profiler +{ + /// + /// Main class for Profiler Service functionality + /// + public interface IXEventSession + { + /// + /// Reads XEvent XML from the default session target + /// + string GetTargetXml(); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/IXEventSessionFactory.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IXEventSessionFactory.cs new file mode 100644 index 00000000..24e46152 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/IXEventSessionFactory.cs @@ -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.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.XEvent; +using Microsoft.SqlTools.ServiceLayer.Connection; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler +{ + /// + /// Main class for Profiler Service functionality + /// + public interface IXEventSessionFactory + { + /// + /// Create a new XEvent session + /// + IXEventSession CreateXEventSession(ConnectionInfo connInfo); + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerService.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerService.cs new file mode 100644 index 00000000..78dd6177 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerService.cs @@ -0,0 +1,246 @@ +// +// 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.Collections.Specialized; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.XEvent; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.Hosting.Protocol.Contracts; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.Hosting; +using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; +using Microsoft.SqlTools.Utility; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler +{ + /// + /// Main class for Profiler Service functionality + /// + public sealed class ProfilerService : IDisposable, IXEventSessionFactory, IProfilerSessionListener + { + private bool disposed; + + private ConnectionService connectionService = null; + + private ProfilerSessionMonitor monitor = new ProfilerSessionMonitor(); + + private static readonly Lazy instance = new Lazy(() => new ProfilerService()); + + /// + /// Construct a new ProfilerService instance with default parameters + /// + public ProfilerService() + { + this.XEventSessionFactory = this; + } + + /// + /// Gets the singleton instance object + /// + public static ProfilerService Instance + { + get { return instance.Value; } + } + + /// + /// Internal for testing purposes only + /// + internal ConnectionService ConnectionServiceInstance + { + get + { + if (connectionService == null) + { + connectionService = ConnectionService.Instance; + } + return connectionService; + } + + set + { + connectionService = value; + } + } + + /// + /// XEvent session factory. Internal to allow mocking in unit tests. + /// + internal IXEventSessionFactory XEventSessionFactory { get; set; } + + /// + /// Session monitor instance + /// + internal ProfilerSessionMonitor SessionMonitor + { + get + { + return this.monitor; + } + } + + /// + /// Service host object for sending/receiving requests/events. + /// Internal for testing purposes. + /// + internal IProtocolEndpoint ServiceHost + { + get; + set; + } + + /// + /// Initializes the Profiler Service instance + /// + public void InitializeService(ServiceHost serviceHost) + { + this.ServiceHost = serviceHost; + this.ServiceHost.SetRequestHandler(StartProfilingRequest.Type, HandleStartProfilingRequest); + this.ServiceHost.SetRequestHandler(StopProfilingRequest.Type, HandleStopProfilingRequest); + + this.SessionMonitor.AddSessionListener(this); + } + + /// + /// Handle request to start a profiling session + /// + internal async Task HandleStartProfilingRequest(StartProfilingParams parameters, RequestContext requestContext) + { + try + { + var result = new StartProfilingResult(); + ConnectionInfo connInfo; + ConnectionServiceInstance.TryFindConnection( + parameters.OwnerUri, + out connInfo); + + if (connInfo != null) + { + ProfilerSession session = StartSession(connInfo); + result.SessionId = session.SessionId; + result.Succeeded = true; + } + else + { + result.Succeeded = false; + result.ErrorMessage = SR.ProfilerConnectionNotFound; + } + + await requestContext.SendResult(result); + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + /// + /// Handle request to stop a profiling session + /// + internal async Task HandleStopProfilingRequest(StopProfilingParams parameters, RequestContext requestContext) + { + try + { + monitor.StopMonitoringSession(parameters.SessionId); + await requestContext.SendResult(new StopProfilingResult + { + Succeeded = true + }); + } + catch (Exception e) + { + await requestContext.SendError(e); + } + } + + /// + /// Starts a new profiler session for the provided connection + /// + internal ProfilerSession StartSession(ConnectionInfo connInfo) + { + // create a new XEvent session and Profiler session + var xeSession = this.XEventSessionFactory.CreateXEventSession(connInfo); + var profilerSession = new ProfilerSession() + { + SessionId = Guid.NewGuid().ToString(), + XEventSession = xeSession + }; + + // start monitoring the profiler session + monitor.StartMonitoringSession(profilerSession); + + return profilerSession; + } + + /// + /// Create a new XEvent sessions per the IXEventSessionFactory contract + /// + public IXEventSession CreateXEventSession(ConnectionInfo connInfo) + { + var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); + SqlStoreConnection connection = new SqlStoreConnection(sqlConnection); + Session session = ProfilerService.GetOrCreateSession(connection, "Profiler"); + + // create xevent session wrapper + return new XEventSession() + { + Session = session + }; + } + + /// + /// Gets an existing XEvent session or creates one if no matching session exists. + /// Also starts the session if it isn't currently running + /// + private static Session GetOrCreateSession(SqlStoreConnection connection, string sessionName) + { + XEStore store = new XEStore(connection); + Session session = store.Sessions["Profiler"]; + // start the session if it isn't already running + if (session != null && !session.IsRunning) + { + session.Start(); + } + return session; + } + + /// + /// Callback when profiler events are available + /// + public void EventsAvailable(string sessionId, List events) + { + // pass the profiler events on to the client + this.ServiceHost.SendEvent( + ProfilerEventsAvailableNotification.Type, + new ProfilerEventsAvailableParams() + { + SessionId = sessionId, + Events = events + }); + } + + /// + /// Disposes the Profiler Service + /// + public void Dispose() + { + if (!disposed) + { + disposed = true; + } + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSession.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSession.cs new file mode 100644 index 00000000..c661bf8f --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSession.cs @@ -0,0 +1,127 @@ +// +// 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.SqlServer.Management.XEvent; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler +{ + /// + /// Profiler session class + /// + public class ProfilerSession + { + private static readonly TimeSpan DefaultPollingDelay = TimeSpan.FromSeconds(1); + private object pollingLock = new object(); + private bool isPolling = false; + private DateTime lastPollTime = DateTime.Now.Subtract(DefaultPollingDelay); + private TimeSpan pollingDelay = DefaultPollingDelay; + private ProfilerEvent lastSeenEvent = null; + + /// + /// Unique ID for the session + /// + public string SessionId { get; set; } + + /// + /// Connection to use for the session + /// + public ConnectionInfo ConnectionInfo { get; set; } + + /// + /// Underlying XEvent session wrapper + /// + public IXEventSession XEventSession { get; set; } + + /// + /// Try to set the session into polling mode if criteria is meet + /// + /// True if session set to polling mode, False otherwise + public bool TryEnterPolling() + { + lock (this.pollingLock) + { + if (!this.isPolling && DateTime.Now.Subtract(this.lastPollTime) >= pollingDelay) + { + this.isPolling = true; + this.lastPollTime = DateTime.Now; + return true; + } + else + { + return false; + } + } + } + + /// + /// Is the session currently being polled + /// + public bool IsPolling + { + get + { + return this.isPolling; + } + set + { + lock (this.pollingLock) + { + this.isPolling = value; + } + } + } + + /// + /// The delay between session polls + /// + public TimeSpan PollingDelay + { + get + { + return pollingDelay; + } + } + + /// + /// Filter the event list to not include previously seen events + /// + public List FilterOldEvents(List events) + { + if (lastSeenEvent != null) + { + // find the last event we've previously seen + bool foundLastEvent = false; + int idx = events.Count; + while (--idx >= 0) + { + if (events[idx].Equals(lastSeenEvent)) + { + foundLastEvent = true; + break; + } + } + + // remove all the events we've seen before + if (foundLastEvent) + { + events.RemoveRange(0, idx + 1); + } + } + + // save the last event so we know where to clean-up the list from next time + if (events.Count > 0) + { + lastSeenEvent = events.LastOrDefault(); + } + + return events; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSessionMonitor.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSessionMonitor.cs new file mode 100644 index 00000000..3ffbfb58 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/ProfilerSessionMonitor.cs @@ -0,0 +1,201 @@ +// +// 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.Data.SqlClient; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using Microsoft.SqlServer.Management.Sdk.Sfc; +using Microsoft.SqlServer.Management.XEvent; +using Microsoft.SqlTools.ServiceLayer.Connection.Contracts; +using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler +{ + /// + /// Classs to monitor active profiler sessions + /// + public class ProfilerSessionMonitor : IProfilerSessionMonitor + { + private const int PollingLoopDelay = 1000; + + private object sessionsLock = new object(); + + private object listenersLock = new object(); + + private Task processorThread = null; + + private Dictionary monitoredSessions = new Dictionary(); + + private List listeners = new List(); + + /// + /// Registers a session event listener to receive a callback when events arrive + /// + public void AddSessionListener(IProfilerSessionListener listener) + { + lock (this.listenersLock) + { + this.listeners.Add(listener); + } + } + + /// + /// Start monitoring the provided sessions + /// + public bool StartMonitoringSession(ProfilerSession session) + { + lock (this.sessionsLock) + { + // start the monitoring thread + if (this.processorThread == null) + { + this.processorThread = Task.Factory.StartNew(ProcessSessions);; + } + + if (!this.monitoredSessions.ContainsKey(session.SessionId)) + { + this.monitoredSessions.Add(session.SessionId, session); + } + } + + return true; + } + + /// + /// Stop monitoring the session specified by the sessionId + /// + public bool StopMonitoringSession(string sessionId) + { + lock (this.sessionsLock) + { + if (this.monitoredSessions.ContainsKey(sessionId)) + { + ProfilerSession session; + return this.monitoredSessions.Remove(sessionId, out session); + } + else + { + return false; + } + } + } + + /// + /// The core queue processing method + /// + /// + private void ProcessSessions() + { + while (true) + { + lock (this.sessionsLock) + { + foreach (var session in this.monitoredSessions.Values) + { + ProcessSession(session); + } + } + + Thread.Sleep(PollingLoopDelay); + } + } + + /// + /// Process a session for new XEvents if it meets the polling criteria + /// + private void ProcessSession(ProfilerSession session) + { + if (session.TryEnterPolling()) + { + Task.Factory.StartNew(() => + { + var events = PollSession(session); + if (events.Count > 0) + { + SendEventsToListeners(session.SessionId, events); + } + }); + } + } + + private List PollSession(ProfilerSession session) + { + var events = new List(); + try + { + if (session == null || session.XEventSession == null) + { + return events; + } + + var targetXml = session.XEventSession.GetTargetXml(); + + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(targetXml); + + var nodes = xmlDoc.DocumentElement.GetElementsByTagName("event"); + foreach (XmlNode node in nodes) + { + var profilerEvent = ParseProfilerEvent(node); + if (profilerEvent != null) + { + events.Add(profilerEvent); + } + } + } + finally + { + session.IsPolling = false; + } + + return session.FilterOldEvents(events); + } + + /// + /// Notify listeners when new profiler events are available + /// + private void SendEventsToListeners(string sessionId, List events) + { + lock (listenersLock) + { + foreach (var listener in this.listeners) + { + listener.EventsAvailable(sessionId, events); + } + } + } + + /// + /// Parse a single event node from XEvent XML + /// + private ProfilerEvent ParseProfilerEvent(XmlNode node) + { + var name = node.Attributes["name"]; + var timestamp = node.Attributes["timestamp"]; + + var profilerEvent = new ProfilerEvent(name.InnerText, timestamp.InnerText); + + foreach (XmlNode childNode in node.ChildNodes) + { + var childName = childNode.Attributes["name"]; + XmlNode typeNode = childNode.SelectSingleNode("type"); + var typeName = typeNode.Attributes["name"]; + XmlNode valueNode = childNode.SelectSingleNode("value"); + + if (!profilerEvent.Values.ContainsKey(childName.InnerText)) + { + profilerEvent.Values.Add(childName.InnerText, valueNode.InnerText); + } + } + + return profilerEvent; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Profiler/XEventSession.cs b/src/Microsoft.SqlTools.ServiceLayer/Profiler/XEventSession.cs new file mode 100644 index 00000000..3e1d6f01 --- /dev/null +++ b/src/Microsoft.SqlTools.ServiceLayer/Profiler/XEventSession.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System.Linq; +using Microsoft.SqlServer.Management.XEvent; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; + +namespace Microsoft.SqlTools.ServiceLayer.Profiler +{ + public class XEventSession : IXEventSession + { + public Session Session { get; set; } + + public string GetTargetXml() + { + if (this.Session == null) + { + return string.Empty; + } + + // try to read events from the first target + Target defaultTarget = this.Session.Targets.FirstOrDefault(); + return defaultTarget != null ? defaultTarget.GetTargetData() : string.Empty; + } + } +} diff --git a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs index 76e6b3be..7f62f187 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/Scripting/ScriptingService.cs @@ -213,12 +213,12 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting { if (!disposed) { + disposed = true; + foreach (ScriptingScriptOperation operation in this.ActiveOperations.Values) { operation.Dispose(); } - - disposed = true; } } diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/BackupServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/BackupServiceTests.cs index 88498303..eab73614 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/BackupServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/BackupServiceTests.cs @@ -12,6 +12,7 @@ using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Admin; using Microsoft.SqlTools.ServiceLayer.Admin.Contracts; +using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts; using Microsoft.SqlTools.ServiceLayer.FileBrowser; @@ -77,7 +78,7 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; SqlTestDb testDb = SqlTestDb.CreateNew(TestServerType.OnPrem, false, databaseName); var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); - SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(liveConnection.ConnectionInfo); string backupPath = GetDefaultBackupFullPath(service, databaseName, helper.DataContainer, sqlConn); @@ -102,7 +103,7 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; SqlTestDb testDb = SqlTestDb.CreateNew(TestServerType.OnPrem, false, databaseName); var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); - SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(liveConnection.ConnectionInfo); string backupPath = GetDefaultBackupFullPath(service, databaseName, helper.DataContainer, sqlConn); BackupInfo backupInfo = CreateDefaultBackupInfo(databaseName, @@ -135,7 +136,7 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; SqlTestDb testDb = SqlTestDb.CreateNew(TestServerType.OnPrem, false, databaseName); var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); - SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(liveConnection.ConnectionInfo); string backupPath = GetDefaultBackupFullPath(service, databaseName, helper.DataContainer, sqlConn); string certificateName = CreateCertificate(testDb); @@ -187,7 +188,7 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; SqlTestDb testDb = SqlTestDb.CreateNew(TestServerType.OnPrem, false, databaseName); var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); - SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(liveConnection.ConnectionInfo); string backupPath = GetDefaultBackupFullPath(service, databaseName, helper.DataContainer, sqlConn); string certificateName = CreateCertificate(testDb); @@ -239,7 +240,7 @@ CREATE CERTIFICATE {1} WITH SUBJECT = 'Backup Encryption Certificate'; "; // Initialize backup service var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); - SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(liveConnection.ConnectionInfo); DisasterRecoveryService disasterRecoveryService = new DisasterRecoveryService(); BackupConfigInfo backupConfigInfo = disasterRecoveryService.GetBackupConfigInfo(helper.DataContainer, sqlConn, sqlConn.Database); diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/DisasterRecoveryFileValidatorTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/DisasterRecoveryFileValidatorTests.cs index 2b315e26..3d076abf 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/DisasterRecoveryFileValidatorTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/DisasterRecoveryFileValidatorTests.cs @@ -3,15 +3,16 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // +using System; using System.Data.SqlClient; using System.IO; using Microsoft.SqlTools.ServiceLayer.Admin; +using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts; using Microsoft.SqlTools.ServiceLayer.FileBrowser; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Xunit; -using System; namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery { @@ -25,7 +26,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery { var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo("master"); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); - SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(liveConnection.ConnectionInfo); string backupPath = Path.Combine(GetDefaultBackupFolderPath(helper.DataContainer, sqlConn), "master.bak"); string message; @@ -45,7 +46,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery { var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo("master"); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); - SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(liveConnection.ConnectionInfo); string backupPath = GetDefaultBackupFolderPath(helper.DataContainer, sqlConn); bool isFolder; diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs index 6f6ed1a6..41073e9f 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/DisasterRecovery/RestoreDatabaseServiceTests.cs @@ -606,7 +606,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery // Initialize backup service var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo(databaseName, queryTempFile.FilePath); DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); - SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(liveConnection.ConnectionInfo); BackupConfigInfo backupConfigInfo = DisasterRecoveryService.Instance.GetBackupConfigInfo(helper.DataContainer, sqlConn, sqlConn.Database); query = $"create table [test].[{tableNames[0]}] (c1 int)"; @@ -661,9 +661,9 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery // Initialize backup service DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(liveConnection.ConnectionInfo, databaseExists: true); - SqlConnection sqlConn = DisasterRecoveryService.GetSqlConnection(liveConnection.ConnectionInfo); + SqlConnection sqlConn = ConnectionService.OpenSqlConnection(liveConnection.ConnectionInfo); - // Get default backup path + // Get default backup pathS BackupConfigInfo backupConfigInfo = DisasterRecoveryService.Instance.GetBackupConfigInfo(helper.DataContainer, sqlConn, sqlConn.Database); string backupPath = Path.Combine(backupConfigInfo.DefaultBackupFolder, testDb.DatabaseName + ".bak"); diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs index 535b22d8..295de177 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Metadata/MetadataServiceTests.cs @@ -3,17 +3,18 @@ // Licensed under the MIT license. See LICENSE file in the project root for full license information. // -using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; -using Xunit; +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Threading.Tasks; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; using Microsoft.SqlTools.ServiceLayer.Metadata; -using System.Collections.Generic; using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts; -using System.Data.SqlClient; -using System; +using Microsoft.SqlTools.ServiceLayer.Workspace.Contracts; using Moq; -using Microsoft.SqlTools.Hosting.Protocol; -using System.Threading.Tasks; +using Xunit; namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Metadata { @@ -71,7 +72,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Metadata this.testTableName += new Random().Next(1000000, 9999999).ToString(); var result = GetLiveAutoCompleteTestObjects(); - var sqlConn = MetadataService.OpenMetadataConnection(result.ConnectionInfo); + var sqlConn = ConnectionService.OpenSqlConnection(result.ConnectionInfo); Assert.NotNull(sqlConn); CreateTestTable(sqlConn); @@ -101,7 +102,7 @@ namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Metadata this.testTableName += new Random().Next(1000000, 9999999).ToString(); var result = GetLiveAutoCompleteTestObjects(); - var sqlConn = MetadataService.OpenMetadataConnection(result.ConnectionInfo); + var sqlConn = ConnectionService.OpenSqlConnection(result.ConnectionInfo); CreateTestTable(sqlConn); diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj index 4ed1c8bb..c99e8f0a 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Microsoft.SqlTools.ServiceLayer.IntegrationTests.csproj @@ -31,7 +31,7 @@ - + diff --git a/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Profiler/ProfilerServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Profiler/ProfilerServiceTests.cs new file mode 100644 index 00000000..3dd8d61d --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.IntegrationTests/Profiler/ProfilerServiceTests.cs @@ -0,0 +1,90 @@ +// +// 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.Data.SqlClient; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlServer.Management.Smo; +using Microsoft.SqlServer.Management.XEvent; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility; +using Microsoft.SqlTools.ServiceLayer.Profiler; +using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; +using Microsoft.SqlTools.ServiceLayer.Test.Common; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.Profiler +{ + public class ProfilerServiceTests + { + /// + /// Verify that a start profiling request starts a profiling session + /// + [Fact] + public async Task TestHandleStartAndStopProfilingRequests() + { + using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile()) + { + var connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath); + + ProfilerService profilerService = new ProfilerService(); + + // start a new session + var startParams = new StartProfilingParams(); + startParams.OwnerUri = connectionResult.ConnectionInfo.OwnerUri; + startParams.TemplateName = "Standard"; + + string sessionId = null; + var startContext = new Mock>(); + startContext.Setup(rc => rc.SendResult(It.IsAny())) + .Returns((result) => + { + // capture the session id for sending the stop message + sessionId = result.SessionId; + return Task.FromResult(0); + }); + + await profilerService.HandleStartProfilingRequest(startParams, startContext.Object); + + startContext.VerifyAll(); + + // wait a bit for the session monitoring to initialize + Thread.Sleep(TimeSpan.FromHours(1)); + + // stop the session + var stopParams = new StopProfilingParams() + { + SessionId = sessionId + }; + + var stopContext = new Mock>(); + stopContext.Setup(rc => rc.SendResult(It.IsAny())) + .Returns(Task.FromResult(0)); + + await profilerService.HandleStopProfilingRequest(stopParams, stopContext.Object); + + stopContext.VerifyAll(); + } + } + + /// + /// Verify the profiler service XEvent session factory + /// + [Fact] + public void TestCreateXEventSession() + { + var liveConnection = LiveConnectionHelper.InitLiveConnectionInfo("master"); + ProfilerService profilerService = new ProfilerService(); + IXEventSession xeSession = profilerService.CreateXEventSession(liveConnection.ConnectionInfo); + Assert.NotNull(xeSession); + Assert.NotNull(xeSession.GetTargetXml()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/Microsoft.SqlTools.ServiceLayer.Test.Common.csproj b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/Microsoft.SqlTools.ServiceLayer.Test.Common.csproj index 4babb0fd..d2b7fde0 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.Test.Common/Microsoft.SqlTools.ServiceLayer.Test.Common.csproj +++ b/test/Microsoft.SqlTools.ServiceLayer.Test.Common/Microsoft.SqlTools.ServiceLayer.Test.Common.csproj @@ -12,7 +12,7 @@ - + diff --git a/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Microsoft.SqlTools.ServiceLayer.TestDriver.csproj b/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Microsoft.SqlTools.ServiceLayer.TestDriver.csproj index dded9854..2f1b2f20 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Microsoft.SqlTools.ServiceLayer.TestDriver.csproj +++ b/test/Microsoft.SqlTools.ServiceLayer.TestDriver/Microsoft.SqlTools.ServiceLayer.TestDriver.csproj @@ -12,8 +12,7 @@ - - + diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj index a990e2da..8daaa188 100644 --- a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Microsoft.SqlTools.ServiceLayer.UnitTests.csproj @@ -13,7 +13,7 @@ - + diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerServiceTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerServiceTests.cs new file mode 100644 index 00000000..9a9e1eb9 --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerServiceTests.cs @@ -0,0 +1,68 @@ +// +// 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.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlServer.Management.XEvent; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Profiler; +using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; +using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler +{ + /// + /// Unit tests for ProfilerService + /// + public class ProfilerServiceTests + { + /// + /// Test starting a profiling session and receiving event callback + /// + /// + [Fact] + public async Task TestStartProfilingRequest() + { + string sessionId = null; + string testUri = "profiler_uri"; + var requestContext = new Mock>(); + requestContext.Setup(rc => rc.SendResult(It.IsAny())) + .Returns((result) => + { + // capture the session id for sending the stop message + sessionId = result.SessionId; + return Task.FromResult(0); + }); + + var sessionListener = new TestSessionListener(); + + var profilerService = new ProfilerService(); + profilerService.SessionMonitor.AddSessionListener(sessionListener); + profilerService.ConnectionServiceInstance = TestObjects.GetTestConnectionService(); + ConnectionInfo connectionInfo = TestObjects.GetTestConnectionInfo(); + profilerService.ConnectionServiceInstance.OwnerToConnectionMap.Add(testUri, connectionInfo); + profilerService.XEventSessionFactory = new TestXEventSessionFactory(); + + var requestParams = new StartProfilingParams(); + requestParams.OwnerUri = testUri; + requestParams.TemplateName = "Standard"; + + await profilerService.HandleStartProfilingRequest(requestParams, requestContext.Object); + + // wait a bit for profile sessions to be polled + Thread.Sleep(500); + + requestContext.VerifyAll(); + + Assert.Equal(sessionListener.PreviousSessionId, sessionId); + Assert.Equal(sessionListener.PreviousEvents.Count, 1); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerSessionTests.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerSessionTests.cs new file mode 100644 index 00000000..b5227b8b --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerSessionTests.cs @@ -0,0 +1,87 @@ +// +// 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 Microsoft.SqlTools.ServiceLayer.Profiler; +using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler +{ + /// + /// Tests for ProfilerSession class + /// + public class ProfilerSessionTests + { + /// + /// Test the FilterOldEvents method + /// + [Fact] + public void TestFilterOldEvents() + { + // create a profiler session and get some test events + var profilerSession = new ProfilerSession(); + var profilerEvents = ProfilerTestObjects.TestProfilerEvents; + + // filter old events shouldn't filter any new events + var newProfilerEvents = profilerSession.FilterOldEvents(profilerEvents); + Assert.Equal(profilerEvents.Count, newProfilerEvents.Count); + + // filter should now filter all the events since they've been seen before + newProfilerEvents = profilerSession.FilterOldEvents(profilerEvents); + Assert.Equal(newProfilerEvents.Count, 0); + + // add a new event + var newEvent = new ProfilerEvent("new event", "1/1/2017"); + profilerEvents.Add(newEvent); + + // verify we only have the new event when reprocessing the event list + newProfilerEvents = profilerSession.FilterOldEvents(profilerEvents); + Assert.Equal(newProfilerEvents.Count, 1); + Assert.True(newProfilerEvents[0].Equals(newEvent)); + + // process whole list again and verify nothing new is available + newProfilerEvents = profilerSession.FilterOldEvents(profilerEvents); + Assert.Equal(newProfilerEvents.Count, 0); + } + + /// + /// Test the TryEnterPolling method + /// + [Fact] + public void TestTryEnterPolling() + { + DateTime startTime = DateTime.Now; + + // create new profiler session + var profilerSession = new ProfilerSession(); + + // enter the polling block + Assert.True(profilerSession.TryEnterPolling()); + Assert.True(profilerSession.IsPolling); + + // verify we can't enter again + Assert.False(profilerSession.TryEnterPolling()); + + // set polling to false to exit polling block + profilerSession.IsPolling = false; + + bool outsideDelay = DateTime.Now.Subtract(startTime) >= profilerSession.PollingDelay; + + // verify we can only enter again if we're outside polling delay interval + Assert.Equal(profilerSession.TryEnterPolling(), outsideDelay); + + // reset IsPolling in case the delay has elasped on slow machine or while debugging + profilerSession.IsPolling = false; + + // wait for the polling delay to elapse + Thread.Sleep(profilerSession.PollingDelay); + + // verify we can enter the polling block again + Assert.True(profilerSession.TryEnterPolling()); + } + } +} diff --git a/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerTestObjects.cs b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerTestObjects.cs new file mode 100644 index 00000000..aad48a9b --- /dev/null +++ b/test/Microsoft.SqlTools.ServiceLayer.UnitTests/Profiler/ProfilerTestObjects.cs @@ -0,0 +1,204 @@ +// +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SqlServer.Management.XEvent; +using Microsoft.SqlTools.Hosting.Protocol; +using Microsoft.SqlTools.ServiceLayer.Connection; +using Microsoft.SqlTools.ServiceLayer.Profiler; +using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; +using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility; +using Moq; +using Xunit; + +namespace Microsoft.SqlTools.ServiceLayer.UnitTests.Profiler +{ + public static class ProfilerTestObjects + { + public static List TestProfilerEvents + { + get + { + return new List + { + new ProfilerEvent("event1", "1/1/2017"), + new ProfilerEvent("event2", "1/2/2017"), + new ProfilerEvent("event3", "1/3/2017") + }; + } + } + } + + public class TestSessionListener : IProfilerSessionListener + { + public string PreviousSessionId { get; set; } + + public List PreviousEvents { get; set; } + + public void EventsAvailable(string sessionId, List events) + { + this.PreviousSessionId = sessionId; + this.PreviousEvents = events; + } + } + + public class TestXEventSession : IXEventSession + { + private string testXEventXml = + "" + + " " + + " " + + " " + + " 51" + + " " + + " " + + " " + + " false" + + " " + + " " + + " " + + " 1" + + " " + + " " + + " " + + " 4096" + + " " + + " " + + " " + + " 0" + + " " + + " " + + " " + + " 2" + + " " + + " " + + " " + + " 191053000" + + " " + + " " + + " " + + " 4680" + + " " + + " " + + " " + + " 2000002838f4010000000000" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " 01" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " 01" + + " " + + " " + + " " + + " 56" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " 930958063" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " A2873402-C433-4D1F-94C4-9CA99749453E-0" + + " " + + " " + + " " + + " 770C3538-EC3F-4A27-86A9-31A2FC777DBC-1" + + " " + + " " + + ""; + + public string GetTargetXml() + { + return testXEventXml; + } + } + + public class TestXEventSessionFactory : IXEventSessionFactory + { + public IXEventSession CreateXEventSession(ConnectionInfo connInfo) + { + return new TestXEventSession(); + } + } +}