From 3c915a92f6fbfafc3ea7802266b98d41ccf65c6f Mon Sep 17 00:00:00 2001 From: Karl Burtram Date: Fri, 16 Nov 2018 12:07:05 -0800 Subject: [PATCH] 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 --- .../LanguageServices/BindingQueue.cs | 41 +++++++++-- .../LanguageServices/ConnectedBindingQueue.cs | 11 ++- .../Contracts/CloseSessionRequest.cs | 33 +++++++++ .../ObjectExplorer/Nodes/TreeNode.cs | 5 +- .../ObjectExplorer/ObjectExplorerService.cs | 70 +++++++++++++------ 5 files changed, 124 insertions(+), 36 deletions(-) diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs index 9af57517..79c96901 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/BindingQueue.cs @@ -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; + /// /// 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); + } } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs index aac80371..29cdf503 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/LanguageServices/ConnectedBindingQueue.cs @@ -88,9 +88,8 @@ namespace Microsoft.SqlTools.ServiceLayer.LanguageServices /// Generate a unique key based on the ConnectionInfo object /// /// - 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); } /// @@ -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) diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Contracts/CloseSessionRequest.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Contracts/CloseSessionRequest.cs index d583555f..da7a64eb 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Contracts/CloseSessionRequest.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Contracts/CloseSessionRequest.cs @@ -39,6 +39,29 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts public string SessionId { get; set; } } + /// + /// Information returned when a session is disconnected. + /// Contains success information and a + /// + public class SessionDisconnectedParameters + { + /// + /// Boolean indicating if the connection was successful + /// + public bool Success { get; set; } + + /// + /// Unique ID to use when sending any requests for objects in the + /// tree under the node + /// + public string SessionId { get; set; } + + /// + /// Error message returned from the engine for a object explorer session failure reason, if any. + /// + public string ErrorMessage { get; set; } + } + /// /// 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 Type = RequestType.Create("objectexplorer/closesession"); } + + /// + /// Session disconnected notification + /// + public class SessionDisconnectedNotification + { + public static readonly + EventType Type = + EventType.Create("objectexplorer/sessiondisconnected"); + } } diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/TreeNode.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/TreeNode.cs index 0bdc18fd..97f5af57 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/TreeNode.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/Nodes/TreeNode.cs @@ -323,10 +323,11 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes SmoQueryContext context = this.GetContextAs(); bool includeSystemObjects = context != null && context.Database != null ? DatabaseUtils.IsSystemDatabaseConnection(context.Database.Name) : true; - if (children.IsPopulating || context == null) - return; + { + return; + } children.Clear(); BeginChildrenInit(); diff --git a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/ObjectExplorerService.cs b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/ObjectExplorerService.cs index c8730ea0..36776550 100644 --- a/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/ObjectExplorerService.cs +++ b/src/Microsoft.SqlTools.ServiceLayer/ObjectExplorer/ObjectExplorerService.cs @@ -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; } - } + } /// /// 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 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 /// Internal for testing purposes only 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 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;