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