Send a Object Explorer session disconnect message on socket exceptions (#739)

* WIP

* WIP 2

* Send disconnect message o binding exception

* Add a try catch around the binding queue error handler
This commit is contained in:
Karl Burtram
2018-11-16 12:07:05 -08:00
committed by GitHub
parent 7f28f249de
commit 3c915a92f6
5 changed files with 124 additions and 36 deletions

View File

@@ -5,12 +5,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Net.Sockets;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlTools.Utility;
using System.Linq;
using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.ServiceLayer.Utility;
using System.Diagnostics; using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{ {
@@ -33,6 +36,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private Task queueProcessorTask; private Task queueProcessorTask;
public delegate void UnhandledExceptionDelegate(string connectionKey, Exception ex);
public event UnhandledExceptionDelegate OnUnhandledException;
/// <summary> /// <summary>
/// Map from context keys to binding context instances /// Map from context keys to binding context instances
/// Internal for testing purposes only /// Internal for testing purposes only
@@ -350,9 +357,26 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{ {
Logger.Write(TraceEventType.Error, "Unexpected exception on the binding queue: " + ex.ToString()); Logger.Write(TraceEventType.Error, "Unexpected exception on the binding queue: " + ex.ToString());
if (queueItem.ErrorHandler != null) if (queueItem.ErrorHandler != null)
{
try
{ {
result = queueItem.ErrorHandler(ex); result = queueItem.ErrorHandler(ex);
} }
catch (Exception ex2)
{
Logger.Write(TraceEventType.Error, "Unexpected exception in binding queue error handler: " + ex2.ToString());
}
}
if (IsExceptionOfType(ex, typeof(SqlException)) || IsExceptionOfType(ex, typeof(SocketException)))
{
if (this.OnUnhandledException != null)
{
this.OnUnhandledException(queueItem.Key, ex);
}
RemoveBindingContext(queueItem.Key);
}
} }
}); });
@@ -469,5 +493,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
} }
} }
} }
private bool IsExceptionOfType(Exception ex, Type t)
{
return ex.GetType() == t || (ex.InnerException != null && ex.InnerException.GetType() == t);
}
} }
} }

View File

@@ -88,9 +88,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
/// Generate a unique key based on the ConnectionInfo object /// Generate a unique key based on the ConnectionInfo object
/// </summary> /// </summary>
/// <param name="connInfo"></param> /// <param name="connInfo"></param>
private string GetConnectionContextKey(ConnectionInfo connInfo) internal static string GetConnectionContextKey(ConnectionDetails details)
{ {
ConnectionDetails details = connInfo.ConnectionDetails;
string key = string.Format("{0}_{1}_{2}_{3}", string key = string.Format("{0}_{1}_{2}_{3}",
details.ServerName ?? "NULL", details.ServerName ?? "NULL",
details.DatabaseName ?? "NULL", details.DatabaseName ?? "NULL",
@@ -108,7 +107,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
key += "_" + details.GroupId; key += "_" + details.GroupId;
} }
return key; return Uri.EscapeUriString(key);
} }
/// <summary> /// <summary>
@@ -158,7 +157,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
public void RemoveBindigContext(ConnectionInfo connInfo) public void RemoveBindigContext(ConnectionInfo connInfo)
{ {
string connectionKey = GetConnectionContextKey(connInfo); string connectionKey = GetConnectionContextKey(connInfo.ConnectionDetails);
if (BindingContextExists(connectionKey)) if (BindingContextExists(connectionKey))
{ {
RemoveBindingContext(connectionKey); RemoveBindingContext(connectionKey);
@@ -178,7 +177,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
} }
// lookup the current binding context // lookup the current binding context
string connectionKey = GetConnectionContextKey(connInfo); string connectionKey = GetConnectionContextKey(connInfo.ConnectionDetails);
if (BindingContextExists(connectionKey)) if (BindingContextExists(connectionKey))
{ {
if (overwrite) if (overwrite)

View File

@@ -39,6 +39,29 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts
public string SessionId { get; set; } public string SessionId { get; set; }
} }
/// <summary>
/// Information returned when a session is disconnected.
/// Contains success information and a <see cref="SessionId"/>
/// </summary>
public class SessionDisconnectedParameters
{
/// <summary>
/// Boolean indicating if the connection was successful
/// </summary>
public bool Success { get; set; }
/// <summary>
/// Unique ID to use when sending any requests for objects in the
/// tree under the node
/// </summary>
public string SessionId { get; set; }
/// <summary>
/// Error message returned from the engine for a object explorer session failure reason, if any.
/// </summary>
public string ErrorMessage { get; set; }
}
/// <summary> /// <summary>
/// Establishes an Object Explorer tree session for a specific connection. /// Establishes an Object Explorer tree session for a specific connection.
/// This will create a connection to a specific server or database, register /// This will create a connection to a specific server or database, register
@@ -50,4 +73,14 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts
RequestType<CloseSessionParams, CloseSessionResponse> Type = RequestType<CloseSessionParams, CloseSessionResponse> Type =
RequestType<CloseSessionParams, CloseSessionResponse>.Create("objectexplorer/closesession"); RequestType<CloseSessionParams, CloseSessionResponse>.Create("objectexplorer/closesession");
} }
/// <summary>
/// Session disconnected notification
/// </summary>
public class SessionDisconnectedNotification
{
public static readonly
EventType<SessionDisconnectedParameters> Type =
EventType<SessionDisconnectedParameters>.Create("objectexplorer/sessiondisconnected");
}
} }

View File

@@ -324,9 +324,10 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
SmoQueryContext context = this.GetContextAs<SmoQueryContext>(); SmoQueryContext context = this.GetContextAs<SmoQueryContext>();
bool includeSystemObjects = context != null && context.Database != null ? DatabaseUtils.IsSystemDatabaseConnection(context.Database.Name) : true; bool includeSystemObjects = context != null && context.Database != null ? DatabaseUtils.IsSystemDatabaseConnection(context.Database.Name) : true;
if (children.IsPopulating || context == null) if (children.IsPopulating || context == null)
{
return; return;
}
children.Clear(); children.Clear();
BeginChildrenInit(); BeginChildrenInit();

View File

@@ -8,10 +8,12 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Composition; using System.Composition;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlTools.Extensibility; using Microsoft.SqlTools.Extensibility;
using Microsoft.SqlTools.Hosting; using Microsoft.SqlTools.Hosting;
using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.Hosting.Protocol;
@@ -25,8 +27,6 @@ using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace; using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.Utility; using Microsoft.SqlTools.Utility;
using Microsoft.SqlServer.Management.Common;
using System.Diagnostics;
namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{ {
@@ -132,6 +132,9 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{ {
Logger.Write(TraceEventType.Verbose, "ObjectExplorer service initialized"); Logger.Write(TraceEventType.Verbose, "ObjectExplorer service initialized");
this.serviceHost = serviceHost; this.serviceHost = serviceHost;
this.ConnectedBindingQueue.OnUnhandledException += OnUnhandledException;
// Register handlers for requests // Register handlers for requests
serviceHost.SetRequestHandler(CreateSessionRequest.Type, HandleCreateSessionRequest); serviceHost.SetRequestHandler(CreateSessionRequest.Type, HandleCreateSessionRequest);
serviceHost.SetRequestHandler(ExpandRequest.Type, HandleExpandRequest); serviceHost.SetRequestHandler(ExpandRequest.Type, HandleExpandRequest);
@@ -513,7 +516,6 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
} }
} }
private async Task<ConnectionCompleteParams> Connect(ConnectParams connectParams, string uri) private async Task<ConnectionCompleteParams> Connect(ConnectParams connectParams, string uri)
{ {
string connectionErrorMessage = string.Empty; string connectionErrorMessage = string.Empty;
@@ -552,6 +554,18 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
await serviceHost.SendEvent(CreateSessionCompleteNotification.Type, result); await serviceHost.SendEvent(CreateSessionCompleteNotification.Type, result);
} }
internal async Task SendSessionDisconnectedNotification(string uri, bool success, string errorMessage)
{
Logger.Write(TraceEventType.Information, $"OE session disconnected: {errorMessage}");
SessionDisconnectedParameters result = new SessionDisconnectedParameters()
{
Success = success,
ErrorMessage = errorMessage,
SessionId = uri
};
await serviceHost.SendEvent(SessionDisconnectedNotification.Type, result);
}
private void RunExpandTask(ObjectExplorerSession session, ExpandParams expandParams, bool forceRefresh = false) private void RunExpandTask(ObjectExplorerSession session, ExpandParams expandParams, bool forceRefresh = false)
{ {
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
@@ -627,22 +641,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
/// <remarks>Internal for testing purposes only</remarks> /// <remarks>Internal for testing purposes only</remarks>
internal static string GenerateUri(ConnectionDetails details) internal static string GenerateUri(ConnectionDetails details)
{ {
Validate.IsNotNull("details", details); return ConnectedBindingQueue.GetConnectionContextKey(details);
string uri = string.Format(CultureInfo.InvariantCulture, "{0}{1}", uriPrefix, Uri.EscapeUriString(details.ServerName));
uri = AppendIfExists(uri, "databaseName", details.DatabaseName);
uri = AppendIfExists(uri, "user", details.UserName);
uri = AppendIfExists(uri, "groupId", details.GroupId);
uri = AppendIfExists(uri, "displayName", details.DatabaseDisplayName);
return uri;
}
private static string AppendIfExists(string uri, string propertyName, string propertyValue)
{
if (!string.IsNullOrEmpty(propertyValue))
{
uri += string.Format(CultureInfo.InvariantCulture, ";{0}={1}", propertyName, Uri.EscapeUriString(propertyValue));
}
return uri;
} }
public IEnumerable<ChildFactory> GetApplicableChildFactories(TreeNode item) public IEnumerable<ChildFactory> GetApplicableChildFactories(TreeNode item)
@@ -742,10 +741,37 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{ {
if (bindingQueue != null) if (bindingQueue != null)
{ {
bindingQueue.OnUnhandledException -= OnUnhandledException;
bindingQueue.Dispose(); bindingQueue.Dispose();
} }
} }
private async void OnUnhandledException(string queueKey, Exception ex)
{
string sessionUri = LookupUriFromQueueKey(queueKey);
if (!string.IsNullOrWhiteSpace(sessionUri))
{
await SendSessionDisconnectedNotification(uri: sessionUri, success: false, errorMessage: ex.ToString());
}
}
private string LookupUriFromQueueKey(string queueKey)
{
foreach (var session in this.sessionMap.Values)
{
var connInfo = session.ConnectionInfo;
if (connInfo != null)
{
string currentKey = ConnectedBindingQueue.GetConnectionContextKey(connInfo.ConnectionDetails);
if (queueKey == currentKey)
{
return session.Uri;
}
}
}
return string.Empty;
}
internal class ObjectExplorerSession internal class ObjectExplorerSession
{ {
private ConnectionService connectionService; private ConnectionService connectionService;