// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // #nullable disable using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.SqlServer.Management.Sdk.Sfc; using Microsoft.SqlServer.Management.XEvent; using Microsoft.SqlServer.Management.XEventDbScoped; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.Profiler.Contracts; using Microsoft.SqlTools.ServiceLayer.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 { 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(CreateXEventSessionRequest.Type, HandleCreateXEventSessionRequest, true); this.ServiceHost.SetRequestHandler(StartProfilingRequest.Type, HandleStartProfilingRequest, true); this.ServiceHost.SetRequestHandler(StopProfilingRequest.Type, HandleStopProfilingRequest, true); this.ServiceHost.SetRequestHandler(PauseProfilingRequest.Type, HandlePauseProfilingRequest, true); this.ServiceHost.SetRequestHandler(GetXEventSessionsRequest.Type, HandleGetXEventSessionsRequest, true); this.ServiceHost.SetRequestHandler(DisconnectSessionRequest.Type, HandleDisconnectSessionRequest, true); this.SessionMonitor.AddSessionListener(this); } /// /// Handle request to start a profiling session /// internal async Task HandleCreateXEventSessionRequest(CreateXEventSessionParams parameters, RequestContext requestContext) { ConnectionInfo connInfo; ConnectionServiceInstance.TryFindConnection( parameters.OwnerUri, out connInfo); if (connInfo == null) { throw new Exception(SR.ProfilerConnectionNotFound); } else if (parameters.SessionName == null) { throw new ArgumentNullException("SessionName"); } else if (parameters.Template == null) { throw new ArgumentNullException("Template"); } else { IXEventSession xeSession = null; // first check whether the session with the given name already exists. // if so skip the creation part. An exception will be thrown if no session with given name can be found, // and it can be ignored. try { xeSession = this.XEventSessionFactory.GetXEventSession(parameters.SessionName, connInfo); } catch { } // create a new XEvent session and Profiler session xeSession ??= this.XEventSessionFactory.CreateXEventSession(parameters.Template.CreateStatement, parameters.SessionName, connInfo); // start monitoring the profiler session monitor.StartMonitoringSession(parameters.OwnerUri, xeSession); var result = new CreateXEventSessionResult(); await requestContext.SendResult(result); SessionCreatedNotification(parameters.OwnerUri, parameters.SessionName, parameters.Template.Name); } } /// /// Handle request to start a profiling session /// internal async Task HandleStartProfilingRequest(StartProfilingParams parameters, RequestContext requestContext) { ConnectionInfo connInfo; ConnectionServiceInstance.TryFindConnection( parameters.OwnerUri, out connInfo); if (connInfo != null) { // create a new XEvent session and Profiler session var xeSession = this.XEventSessionFactory.GetXEventSession(parameters.SessionName, connInfo); // start monitoring the profiler session monitor.StartMonitoringSession(parameters.OwnerUri, xeSession); var result = new StartProfilingResult(); await requestContext.SendResult(result); } else { throw new Exception(SR.ProfilerConnectionNotFound); } } /// /// Handle request to stop a profiling session /// internal async Task HandleStopProfilingRequest(StopProfilingParams parameters, RequestContext requestContext) { ProfilerSession session; monitor.StopMonitoringSession(parameters.OwnerUri, out session); if (session != null) { // Occasionally we might see the InvalidOperationException due to a read is // in progress, add the following retry logic will solve the problem. int remainingAttempts = 3; while (true) { try { session.XEventSession.Stop(); await requestContext.SendResult(new StopProfilingResult { }); break; } catch (InvalidOperationException) { remainingAttempts--; if (remainingAttempts == 0) { throw; } Thread.Sleep(500); } } } else { throw new Exception(SR.SessionNotFound); } } /// /// Handle request to pause a profiling session /// internal async Task HandlePauseProfilingRequest(PauseProfilingParams parameters, RequestContext requestContext) { monitor.PauseViewer(parameters.OwnerUri); await requestContext.SendResult(new PauseProfilingResult { }); } /// /// Handle request to pause a profiling session /// internal async Task HandleGetXEventSessionsRequest(GetXEventSessionsParams parameters, RequestContext requestContext) { var result = new GetXEventSessionsResult(); ConnectionInfo connInfo; ConnectionServiceInstance.TryFindConnection( parameters.OwnerUri, out connInfo); if (connInfo == null) { await requestContext.SendError(new Exception(SR.ProfilerConnectionNotFound)); } else { List sessions = GetXEventSessionList(parameters.OwnerUri, connInfo); result.Sessions = sessions; await requestContext.SendResult(result); } } /// /// Handle request to disconnect a session /// internal async Task HandleDisconnectSessionRequest(DisconnectSessionParams parameters, RequestContext requestContext) { monitor.StopMonitoringSession(parameters.OwnerUri, out _); } /// /// Gets a list of all running XEvent Sessions /// /// /// A list of the names of all running XEvent sessions /// internal List GetXEventSessionList(string ownerUri, ConnectionInfo connInfo) { var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); SqlStoreConnection connection = new SqlStoreConnection(sqlConnection); BaseXEStore store = CreateXEventStore(connInfo, connection); // get session names from the session list List results = store.Sessions.Aggregate(new List(), (result, next) => { result.Add(next.Name); return result; }); return results; } private static BaseXEStore CreateXEventStore(ConnectionInfo connInfo, SqlStoreConnection connection) { BaseXEStore store = null; if (connInfo.IsCloud) { if (DatabaseUtils.IsSystemDatabaseConnection(connInfo.ConnectionDetails.DatabaseName)) { throw new NotSupportedException(SR.AzureSystemDbProfilingError); } store = new DatabaseXEStore(connection, connInfo.ConnectionDetails.DatabaseName); } else { store = new XEStore(connection); } return store; } /// /// Gets an XEvent session with the given name per the IXEventSessionFactory contract /// Also starts the session if it isn't currently running /// public IXEventSession GetXEventSession(string sessionName, ConnectionInfo connInfo) { var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); SqlStoreConnection connection = new SqlStoreConnection(sqlConnection); BaseXEStore store = CreateXEventStore(connInfo, connection); Session session = store.Sessions[sessionName] ?? throw new Exception(SR.SessionNotFound); // start the session if it isn't already running if (session != null && !session.IsRunning) { session.Start(); } // create xevent session wrapper return new XEventSession() { Session = session }; } /// /// Creates and starts an XEvent session with the given name and create statement per the IXEventSessionFactory contract /// public IXEventSession CreateXEventSession(string createStatement, string sessionName, ConnectionInfo connInfo) { var sqlConnection = ConnectionService.OpenSqlConnection(connInfo); SqlStoreConnection connection = new SqlStoreConnection(sqlConnection); BaseXEStore store = CreateXEventStore(connInfo, connection); Session session = store.Sessions[sessionName]; // session shouldn't already exist if (session != null) { throw new Exception(SR.SessionAlreadyExists(sessionName)); } var statement = createStatement.Replace("{sessionName}", sessionName); connection.ServerConnection.ExecuteNonQuery(statement); store.Refresh(); session = store.Sessions[sessionName]; if (session == null) { throw new Exception(SR.SessionNotFound); } if (!session.IsRunning) { session.Start(); } // create xevent session wrapper return new XEventSession() { Session = store.Sessions[sessionName] }; } /// /// Callback when profiler events are available /// public void EventsAvailable(string sessionId, List events, bool eventsLost) { // pass the profiler events on to the client this.ServiceHost.SendEvent( ProfilerEventsAvailableNotification.Type, new ProfilerEventsAvailableParams() { OwnerUri = sessionId, Events = events, EventsLost = eventsLost }); } /// /// Callback when the XEvent session is closed unexpectedly /// public void SessionStopped(string viewerId, int sessionId) { // notify the client that their session closed this.ServiceHost.SendEvent( ProfilerSessionStoppedNotification.Type, new ProfilerSessionStoppedParams() { OwnerUri = viewerId, SessionId = sessionId }); } /// /// Callback when a new session is created /// public void SessionCreatedNotification(string viewerId, string sessionName, string templateName) { // pass the profiler events on to the client this.ServiceHost.SendEvent( ProfilerSessionCreatedNotification.Type, new ProfilerSessionCreatedParams() { OwnerUri = viewerId, SessionName = sessionName, TemplateName = templateName }); } /// /// Disposes the Profiler Service /// public void Dispose() { if (!disposed) { disposed = true; } } } }