// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections.Concurrent; using Microsoft.Data.SqlClient; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.FileBrowser.Contracts; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.LanguageServices; using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.FileBrowser { /// /// Main class for file browser service /// public sealed class FileBrowserService: IDisposable { private static readonly Lazy LazyInstance = new Lazy(() => new FileBrowserService()); public static FileBrowserService Instance => LazyInstance.Value; // Cache file browser operations for expanding node request private readonly ConcurrentDictionary ownerToFileBrowserMap = new ConcurrentDictionary(); private readonly ConcurrentDictionary validatePathsCallbackMap = new ConcurrentDictionary(); private ConnectionService connectionService; private ConnectedBindingQueue fileBrowserQueue = new ConnectedBindingQueue(needsMetadata: false); private static int DefaultTimeout = 120000; private string serviceName = "FileBrowser"; /// /// Signature for callback method that validates the selected file paths /// /// public delegate bool ValidatePathsCallback(FileBrowserValidateEventArgs eventArgs, out string errorMessage); internal ConnectionService ConnectionServiceInstance { get { if (connectionService == null) { connectionService = ConnectionService.Instance; connectionService.RegisterConnectedQueue(this.serviceName, this.fileBrowserQueue); } return connectionService; } set { connectionService = value; } } /// /// Register validate path callback /// /// /// public void RegisterValidatePathsCallback(string service, ValidatePathsCallback callback) { validatePathsCallbackMap.AddOrUpdate(service, callback, (key, oldValue) => callback); } /// /// Initializes the service instance /// /// Service host to register handlers with public void InitializeService(ServiceHost serviceHost) { // Open a file browser serviceHost.SetRequestHandler(FileBrowserOpenRequest.Type, HandleFileBrowserOpenRequest); // Expand a folder node serviceHost.SetRequestHandler(FileBrowserExpandRequest.Type, HandleFileBrowserExpandRequest); // Validate the selected files serviceHost.SetRequestHandler(FileBrowserValidateRequest.Type, HandleFileBrowserValidateRequest); // Close the file browser serviceHost.SetRequestHandler(FileBrowserCloseRequest.Type, HandleFileBrowserCloseRequest); } #region request handlers internal async Task HandleFileBrowserOpenRequest(FileBrowserOpenParams fileBrowserParams, RequestContext requestContext) { try { var task = Task.Run(() => RunFileBrowserOpenTask(fileBrowserParams, requestContext)) .ContinueWithOnFaulted(null); await requestContext.SendResult(true); } catch (Exception ex) { Logger.Write(TraceEventType.Error, "Unexpected exception while handling file browser open request: " + ex.Message); await requestContext.SendResult(false); } } internal async Task HandleFileBrowserExpandRequest(FileBrowserExpandParams fileBrowserParams, RequestContext requestContext) { try { var task = Task.Run(() => RunFileBrowserExpandTask(fileBrowserParams, requestContext)) .ContinueWithOnFaulted(null); await requestContext.SendResult(true); } catch (Exception ex) { Logger.Write(TraceEventType.Error, "Unexpected exception while handling file browser expand request: " + ex.Message); await requestContext.SendResult(false); } } internal async Task HandleFileBrowserValidateRequest(FileBrowserValidateParams fileBrowserParams, RequestContext requestContext) { try { var task = Task.Run(() => RunFileBrowserValidateTask(fileBrowserParams, requestContext)) .ContinueWithOnFaulted(null); await requestContext.SendResult(true); } catch (Exception ex) { Logger.Write(TraceEventType.Error, "Unexpected exception while handling file browser validate request: " + ex.Message); await requestContext.SendResult(false); } } internal async Task HandleFileBrowserCloseRequest( FileBrowserCloseParams fileBrowserParams, RequestContext requestContext) { try { var task = Task.Run(() => RunFileBrowserCloseTask(fileBrowserParams, requestContext)) .ContinueWithOnFaulted(null); await requestContext.SendResult(new FileBrowserCloseResponse() { Succeeded = true }); } catch (Exception ex) { Logger.Write(TraceEventType.Error, "Unexpected exception while handling file browser close request: " + ex.Message); await requestContext.SendResult(new FileBrowserCloseResponse() { Message = ex.Message }); } } #endregion public void Dispose() { this.fileBrowserQueue.Dispose(); } internal async Task RunFileBrowserCloseTask(FileBrowserCloseParams fileBrowserParams, RequestContext requestContext) { FileBrowserCloseResponse result = new FileBrowserCloseResponse(); try { FileBrowserOperation operation = null; ConnectionInfo connInfo; ownerToFileBrowserMap.TryGetValue(fileBrowserParams.OwnerUri, out operation); this.ConnectionServiceInstance.TryFindConnection(fileBrowserParams.OwnerUri, out connInfo); if (operation != null && connInfo != null) { if (!operation.FileTreeCreated) { operation.Cancel(); } // Clear queued items this.fileBrowserQueue.ClearQueuedItems(); // Queue operation to clean up resources QueueItem queueItem = fileBrowserQueue.QueueBindingOperation( key: fileBrowserQueue.AddConnectionContext(connInfo, this.serviceName), bindingTimeout: DefaultTimeout, waitForLockTimeout: DefaultTimeout, bindOperation: (bindingContext, cancelToken) => { FileBrowserOperation removedOperation = null; ownerToFileBrowserMap.TryRemove(fileBrowserParams.OwnerUri, out removedOperation); if (removedOperation != null) { removedOperation.Dispose(); } result.Succeeded = true; return result; }); queueItem.ItemProcessed.WaitOne(); if (queueItem.GetResultAsT() != null) { result = queueItem.GetResultAsT(); } this.fileBrowserQueue.CloseConnections(connInfo.ConnectionDetails.ServerName, connInfo.ConnectionDetails.DatabaseName, DefaultTimeout); } } catch (Exception ex) { Logger.Write(TraceEventType.Error, "Unexpected exception while closing file browser: " + ex.Message); result.Message = ex.Message; } await requestContext.SendEvent(FileBrowserClosedNotification.Type, result); } internal async Task RunFileBrowserOpenTask(FileBrowserOpenParams fileBrowserParams, RequestContext requestContext) { FileBrowserOpenedParams result = new FileBrowserOpenedParams(); FileBrowserOperation browser = null; bool isCancelRequested = false; try { ConnectionInfo connInfo; this.ConnectionServiceInstance.TryFindConnection(fileBrowserParams.OwnerUri, out connInfo); if (connInfo != null) { QueueItem queueItem = fileBrowserQueue.QueueBindingOperation( key: fileBrowserQueue.AddConnectionContext(connInfo, this.serviceName), bindingTimeout: DefaultTimeout, waitForLockTimeout: DefaultTimeout, bindOperation: (bindingContext, cancelToken) => { if (!fileBrowserParams.ChangeFilter) { browser = new FileBrowserOperation(bindingContext.ServerConnection, fileBrowserParams.ExpandPath, fileBrowserParams.FileFilters); } else { ownerToFileBrowserMap.TryGetValue(fileBrowserParams.OwnerUri, out browser); if (browser != null) { browser.Initialize(fileBrowserParams.ExpandPath, fileBrowserParams.FileFilters); } } if (browser != null) { ownerToFileBrowserMap.AddOrUpdate(fileBrowserParams.OwnerUri, browser, (key, value) => browser); // Create file browser tree browser.PopulateFileTree(); if (browser.IsCancellationRequested) { isCancelRequested = true; } else { result.OwnerUri = fileBrowserParams.OwnerUri; result.FileTree = browser.FileTree; result.Succeeded = true; } } return result; }); queueItem.ItemProcessed.WaitOne(); if (queueItem.GetResultAsT() != null) { result = queueItem.GetResultAsT(); } } } catch (Exception ex) { result.Message = ex.Message; } if (!isCancelRequested) { await requestContext.SendEvent(FileBrowserOpenedNotification.Type, result); } } internal async Task RunFileBrowserExpandTask(FileBrowserExpandParams fileBrowserParams, RequestContext requestContext) { FileBrowserExpandedParams result = new FileBrowserExpandedParams(); try { FileBrowserOperation operation = null; ConnectionInfo connInfo; ownerToFileBrowserMap.TryGetValue(fileBrowserParams.OwnerUri, out operation); this.ConnectionServiceInstance.TryFindConnection(fileBrowserParams.OwnerUri, out connInfo); if (operation != null && connInfo != null) { QueueItem queueItem = fileBrowserQueue.QueueBindingOperation( key: fileBrowserQueue.AddConnectionContext(connInfo, this.serviceName), bindingTimeout: DefaultTimeout, waitForLockTimeout: DefaultTimeout, bindOperation: (bindingContext, cancelToken) => { result.ExpandPath = fileBrowserParams.ExpandPath; result.Children = operation.GetChildren(fileBrowserParams.ExpandPath).ToArray(); result.OwnerUri = fileBrowserParams.OwnerUri; result.Succeeded = true; return result; }); queueItem.ItemProcessed.WaitOne(); if (queueItem.GetResultAsT() != null) { result = queueItem.GetResultAsT(); } } } catch (Exception ex) { result.Message = ex.Message; } await requestContext.SendEvent(FileBrowserExpandedNotification.Type, result); } internal async Task RunFileBrowserValidateTask(FileBrowserValidateParams fileBrowserParams, RequestContext requestContext) { FileBrowserValidatedParams result = new FileBrowserValidatedParams(); try { ValidatePathsCallback callback; ConnectionInfo connInfo; this.ConnectionServiceInstance.TryFindConnection(fileBrowserParams.OwnerUri, out connInfo); if (validatePathsCallbackMap.TryGetValue(fileBrowserParams.ServiceType, out callback) && callback != null && connInfo != null && fileBrowserParams.SelectedFiles != null && fileBrowserParams.SelectedFiles.Length > 0) { QueueItem queueItem = fileBrowserQueue.QueueBindingOperation( key: fileBrowserQueue.AddConnectionContext(connInfo, this.serviceName), bindingTimeout: DefaultTimeout, waitForLockTimeout: DefaultTimeout, bindOperation: (bindingContext, cancelToken) => { string errorMessage; result.Succeeded = callback(new FileBrowserValidateEventArgs { ServiceType = fileBrowserParams.ServiceType, OwnerUri = fileBrowserParams.OwnerUri, FilePaths = fileBrowserParams.SelectedFiles }, out errorMessage); result.Message = errorMessage; return result; }); queueItem.ItemProcessed.WaitOne(); if (queueItem.GetResultAsT() != null) { result = queueItem.GetResultAsT(); } } else { result.Succeeded = true; } } catch (Exception ex) { result.Message = ex.Message; } await requestContext.SendEvent(FileBrowserValidatedNotification.Type, result); } } }