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.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlTools.Utility;
using System.Linq;
using Microsoft.SqlTools.ServiceLayer.Utility;
using System.Diagnostics;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
{
@@ -33,6 +36,10 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
private Task queueProcessorTask;
public delegate void UnhandledExceptionDelegate(string connectionKey, Exception ex);
public event UnhandledExceptionDelegate OnUnhandledException;
/// <summary>
/// Map from context keys to binding context instances
/// Internal for testing purposes only
@@ -351,11 +358,28 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
Logger.Write(TraceEventType.Error, "Unexpected exception on the binding queue: " + ex.ToString());
if (queueItem.ErrorHandler != null)
{
result = queueItem.ErrorHandler(ex);
try
{
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);
}
}
});
Task.Run(() =>
{
try
@@ -371,7 +395,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
// if the task didn't complete then call the timeout callback
if (queueItem.TimeoutOperation != null)
{
{
queueItem.Result = queueItem.TimeoutOperation(bindingContext);
}
@@ -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
/// </summary>
/// <param name="connInfo"></param>
private string GetConnectionContextKey(ConnectionInfo connInfo)
{
ConnectionDetails details = connInfo.ConnectionDetails;
internal static string GetConnectionContextKey(ConnectionDetails details)
{
string key = string.Format("{0}_{1}_{2}_{3}",
details.ServerName ?? "NULL",
details.DatabaseName ?? "NULL",
@@ -108,7 +107,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
key += "_" + details.GroupId;
}
return key;
return Uri.EscapeUriString(key);
}
/// <summary>
@@ -158,7 +157,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
public void RemoveBindigContext(ConnectionInfo connInfo)
{
string connectionKey = GetConnectionContextKey(connInfo);
string connectionKey = GetConnectionContextKey(connInfo.ConnectionDetails);
if (BindingContextExists(connectionKey))
{
RemoveBindingContext(connectionKey);
@@ -178,7 +177,7 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices
}
// lookup the current binding context
string connectionKey = GetConnectionContextKey(connInfo);
string connectionKey = GetConnectionContextKey(connInfo.ConnectionDetails);
if (BindingContextExists(connectionKey))
{
if (overwrite)

View File

@@ -39,6 +39,29 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts
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>
/// Establishes an Object Explorer tree session for a specific connection.
/// 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>.Create("objectexplorer/closesession");
}
/// <summary>
/// Session disconnected notification
/// </summary>
public class SessionDisconnectedNotification
{
public static readonly
EventType<SessionDisconnectedParameters> Type =
EventType<SessionDisconnectedParameters>.Create("objectexplorer/sessiondisconnected");
}
}

View File

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

View File

@@ -8,10 +8,12 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Composition;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlTools.Extensibility;
using Microsoft.SqlTools.Hosting;
using Microsoft.SqlTools.Hosting.Protocol;
@@ -25,8 +27,6 @@ using Microsoft.SqlTools.ServiceLayer.SqlContext;
using Microsoft.SqlTools.ServiceLayer.Utility;
using Microsoft.SqlTools.ServiceLayer.Workspace;
using Microsoft.SqlTools.Utility;
using Microsoft.SqlServer.Management.Common;
using System.Diagnostics;
namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{
@@ -73,7 +73,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{
this.bindingQueue = value;
}
}
}
/// <summary>
/// Internal for testing only
@@ -132,6 +132,9 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{
Logger.Write(TraceEventType.Verbose, "ObjectExplorer service initialized");
this.serviceHost = serviceHost;
this.ConnectedBindingQueue.OnUnhandledException += OnUnhandledException;
// Register handlers for requests
serviceHost.SetRequestHandler(CreateSessionRequest.Type, HandleCreateSessionRequest);
serviceHost.SetRequestHandler(ExpandRequest.Type, HandleExpandRequest);
@@ -511,8 +514,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
await SendSessionFailedNotification(uri, ex.Message);
return null;
}
}
}
private async Task<ConnectionCompleteParams> Connect(ConnectParams connectParams, string uri)
{
@@ -552,6 +554,18 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
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)
{
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
@@ -627,24 +641,9 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
/// <remarks>Internal for testing purposes only</remarks>
internal static string GenerateUri(ConnectionDetails details)
{
Validate.IsNotNull("details", 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;
}
return ConnectedBindingQueue.GetConnectionContextKey(details);
}
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)
{
if (ApplicableNodeChildFactories != null)
@@ -742,10 +741,37 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{
if (bindingQueue != null)
{
bindingQueue.OnUnhandledException -= OnUnhandledException;
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
{
private ConnectionService connectionService;