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