Add scripting API implemented by the SqlScriptPublishModel (#316)

Update the ScriptingService to expose new scripting JSON-RPC APIs that use the SqlScriptPublishModel for script generation.

The SqlScriptPublishModel is the model behind the SSMS scripting wizard. To enable scripting for CLI tools, we've ported SqlScriptPublishModel to .NET Core. The SqlScriptPublishModel wraps the SMO scripting APIs for .sql script generation.

1) Added three new requests to the ScriptingService: ScriptingRequest, ScriptingListObjectsRequest, ScriptingCancelRequest.
2) Generating scripts are long running operations, so the ScriptingRequest and ScriptingListObjectsRequest kick off a long running scripting task and return immediately.
3) Long running scripting task reports progress and completion, and can be cancelled by a ScriptingCancelRequest request.
4) Bumped the SMO nuget package to 140.17049.0. This new version contains a signed SSMS_Rel build of SMO with the SqlScriptPublishModel.
5) For testing, adding the Northwind database schema

TODO (in later pull requests)
1) Integrate the new ScriptingService APIs with the ConnectionService
2) Integrate with the metadata support recently added
This commit is contained in:
Brian O'Neill
2017-04-24 16:10:20 -07:00
committed by GitHub
parent e65699ef75
commit 4aac4a4047
42 changed files with 7124 additions and 30 deletions

View File

@@ -4,24 +4,27 @@
//
using System;
using System.Collections.Concurrent;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Hosting;
using Microsoft.SqlTools.ServiceLayer.LanguageServices;
using Microsoft.SqlTools.ServiceLayer.Metadata.Contracts;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Scripting
{
/// <summary>
/// Main class for Scripting Service functionality
/// </summary>
public sealed class ScriptingService
public sealed class ScriptingService : IDisposable
{
private const int ScriptingOperationTimeout = 60000;
@@ -31,9 +34,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting
private static ConnectionService connectionService = null;
private static LanguageService languageServices = null;
private static LanguageService languageServices = null;
/// <summary>
private readonly Lazy<ConcurrentDictionary<string, ScriptingOperation>> operations =
new Lazy<ConcurrentDictionary<string, ScriptingOperation>>(() => new ConcurrentDictionary<string, ScriptingOperation>());
private bool disposed;
/// <summary>
/// Internal for testing purposes only
/// </summary>
internal static ConnectionService ConnectionServiceInstance
@@ -72,6 +80,11 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting
}
}
/// <summary>
/// The collection of active operations
/// </summary>
internal ConcurrentDictionary<string, ScriptingOperation> ActiveOperations => operations.Value;
/// <summary>
/// Initializes the Scripting Service instance
/// </summary>
@@ -80,6 +93,132 @@ namespace Microsoft.SqlTools.ServiceLayer.Scripting
public void InitializeService(ServiceHost serviceHost)
{
serviceHost.SetRequestHandler(ScriptingScriptAsRequest.Type, HandleScriptingScriptAsRequest);
serviceHost.SetRequestHandler(ScriptingRequest.Type, this.HandleScriptExecuteRequest);
serviceHost.SetRequestHandler(ScriptingCancelRequest.Type, this.HandleScriptCancelRequest);
serviceHost.SetRequestHandler(ScriptingListObjectsRequest.Type, this.HandleListObjectsRequest);
// Register handler for shutdown event
serviceHost.RegisterShutdownTask((shutdownParams, requestContext) =>
{
this.Dispose();
return Task.FromResult(0);
});
}
/// <summary>
/// Handles request to execute start the list objects operation.
/// </summary>
private async Task HandleListObjectsRequest(ScriptingListObjectsParams parameters, RequestContext<ScriptingListObjectsResult> requestContext)
{
try
{
ScriptingListObjectsOperation operation = new ScriptingListObjectsOperation(parameters);
operation.CompleteNotification += (sender, e) => this.SendEvent(requestContext, ScriptingListObjectsCompleteEvent.Type, e);
RunTask(requestContext, operation);
await requestContext.SendResult(new ScriptingListObjectsResult { OperationId = operation.OperationId });
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
/// <summary>
/// Handles request to execute start the script operation.
/// </summary>
public async Task HandleScriptExecuteRequest(ScriptingParams parameters, RequestContext<ScriptingResult> requestContext)
{
try
{
ScriptingScriptOperation operation = new ScriptingScriptOperation(parameters);
operation.PlanNotification += (sender, e) => this.SendEvent(requestContext, ScriptingPlanNotificationEvent.Type, e);
operation.ProgressNotification += (sender, e) => this.SendEvent(requestContext, ScriptingProgressNotificationEvent.Type, e);
operation.CompleteNotification += (sender, e) => this.SendEvent(requestContext, ScriptingCompleteEvent.Type, e);
RunTask(requestContext, operation);
await requestContext.SendResult(new ScriptingResult { OperationId = operation.OperationId });
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
/// <summary>
/// Handles request to cancel a script operation.
/// </summary>
public async Task HandleScriptCancelRequest(ScriptingCancelParams parameters, RequestContext<ScriptingCancelResult> requestContext)
{
try
{
ScriptingOperation operation = null;
if (this.ActiveOperations.TryRemove(parameters.OperationId, out operation))
{
operation.Cancel();
}
else
{
Logger.Write(LogLevel.Normal, string.Format("Operation {0} was not found", operation.OperationId));
}
await requestContext.SendResult(new ScriptingCancelResult());
}
catch (Exception e)
{
await requestContext.SendError(e);
}
}
/// <summary>
/// Sends a JSON-RPC event.
/// </summary>
private void SendEvent<TParams>(IEventSender requestContext, EventType<TParams> eventType, TParams parameters)
{
Task.Run(async () => await requestContext.SendEvent(eventType, parameters));
}
/// <summary>
/// Runs the async task that performs the scripting operation.
/// </summary>
private void RunTask<T>(RequestContext<T> context, ScriptingOperation operation)
{
Task.Run(() =>
{
try
{
Debug.Assert(!this.ActiveOperations.ContainsKey(operation.OperationId), "Operation id must be unique");
this.ActiveOperations[operation.OperationId] = operation;
operation.Execute();
}
catch (Exception e)
{
context.SendError(e);
}
finally
{
ScriptingOperation temp;
this.ActiveOperations.TryRemove(operation.OperationId, out temp);
}
});
}
/// <summary>
/// Disposes the scripting service and all active scripting operations.
/// </summary>
public void Dispose()
{
if (!disposed)
{
foreach (ScriptingScriptOperation operation in this.ActiveOperations.Values)
{
operation.Dispose();
}
disposed = true;
}
}
/// <summary>