// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // #nullable disable using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Composition; using System.Diagnostics; using System.Threading.Tasks; using Microsoft.SqlTools.Extensibility; using Microsoft.SqlTools.Hosting; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.QueryExecution.Contracts; using Microsoft.SqlTools.ServiceLayer.QueryExecution.DataStorage; using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.ServiceLayer.QueryExecution { [Export(typeof(IHostedService))] public class SerializationService : HostedService, IComposableService { private ConcurrentDictionary inProgressSerializations; public SerializationService() { inProgressSerializations = new ConcurrentDictionary(); } public override void InitializeService(IProtocolEndpoint serviceHost) { Logger.Write(TraceEventType.Verbose, "SerializationService initialized"); serviceHost.SetRequestHandler(SerializeStartRequest.Type, HandleSerializeStartRequest, true); serviceHost.SetRequestHandler(SerializeContinueRequest.Type, HandleSerializeContinueRequest, true); } /// /// Begin to process request to save a resultSet to a file in CSV format /// internal Task HandleSerializeStartRequest(SerializeDataStartRequestParams serializeParams, RequestContext requestContext) { // Run in separate thread so that message thread isn't held up by a potentially time consuming file write Task.Run(async () => { await RunSerializeStartRequest(serializeParams, requestContext); }).ContinueWithOnFaulted(async t => await SendErrorAndCleanup(serializeParams?.FilePath, requestContext, t.Exception)); return Task.CompletedTask; } internal async Task RunSerializeStartRequest(SerializeDataStartRequestParams serializeParams, RequestContext requestContext) { try { // Verify we have sensible inputs and there isn't a task running for this file already Validate.IsNotNull(nameof(serializeParams), serializeParams); Validate.IsNotNullOrWhitespaceString("FilePath", serializeParams.FilePath); DataSerializer serializer = null; if (inProgressSerializations.TryGetValue(serializeParams.FilePath, out serializer)) { // Cannot proceed as there is an in progress serialization happening throw new Exception(SR.SerializationServiceRequestInProgress(serializeParams.FilePath)); } // Create a new serializer, save for future calls if needed, and write the request out serializer = new DataSerializer(serializeParams); if (!serializeParams.IsLastBatch) { inProgressSerializations.AddOrUpdate(serializer.FilePath, serializer, (key, old) => serializer); } Logger.Write(TraceEventType.Verbose, "HandleSerializeStartRequest"); SerializeDataResult result = serializer.ProcessRequest(serializeParams); await requestContext.SendResult(result); } catch (Exception ex) { await SendErrorAndCleanup(serializeParams.FilePath, requestContext, ex); } } private async Task SendErrorAndCleanup(string filePath, RequestContext requestContext, Exception ex) { if (filePath != null) { try { DataSerializer removed; inProgressSerializations.TryRemove(filePath, out removed); if (removed != null) { // Flush any contents to disk and remove the writer removed.CloseStreams(); } } catch { // Do not care if there was an error removing this, must always delete if something failed } } await requestContext.SendError(ex.Message); } /// /// Process request to save a resultSet to a file in CSV format /// internal Task HandleSerializeContinueRequest(SerializeDataContinueRequestParams serializeParams, RequestContext requestContext) { // Run in separate thread so that message thread isn't held up by a potentially time consuming file write Task.Run(async () => { await RunSerializeContinueRequest(serializeParams, requestContext); }).ContinueWithOnFaulted(async t => await SendErrorAndCleanup(serializeParams?.FilePath, requestContext, t.Exception)); return Task.CompletedTask; } internal async Task RunSerializeContinueRequest(SerializeDataContinueRequestParams serializeParams, RequestContext requestContext) { try { // Verify we have sensible inputs and some data has already been sent for the file Validate.IsNotNull(nameof(serializeParams), serializeParams); Validate.IsNotNullOrWhitespaceString("FilePath", serializeParams.FilePath); DataSerializer serializer = null; if (!inProgressSerializations.TryGetValue(serializeParams.FilePath, out serializer)) { throw new Exception(SR.SerializationServiceRequestNotFound(serializeParams.FilePath)); } // Write to file and cleanup if needed Logger.Write(TraceEventType.Verbose, "HandleSerializeContinueRequest"); SerializeDataResult result = serializer.ProcessRequest(serializeParams); if (serializeParams.IsLastBatch) { // Cleanup the serializer this.inProgressSerializations.TryRemove(serializer.FilePath, out serializer); } await requestContext.SendResult(result); } catch (Exception ex) { await SendErrorAndCleanup(serializeParams.FilePath, requestContext, ex); } } } class DataSerializer { private IFileStreamWriter writer; private SerializeDataStartRequestParams requestParams; private IReadOnlyList columns; public string FilePath { get; private set; } public DataSerializer(SerializeDataStartRequestParams requestParams) { this.requestParams = requestParams; this.columns = this.MapColumns(requestParams.Columns); this.FilePath = requestParams.FilePath; } private IReadOnlyList MapColumns(ColumnInfo[] columns) { List columnWrappers = new List(); foreach (ColumnInfo column in columns) { DbColumnWrapper wrapper = new DbColumnWrapper(column); columnWrappers.Add(wrapper); } return columnWrappers; } public SerializeDataResult ProcessRequest(ISerializationParams serializeParams) { SerializeDataResult result = new SerializeDataResult(); try { this.WriteData(serializeParams.Rows, serializeParams.IsLastBatch); if (serializeParams.IsLastBatch) { this.CloseStreams(); } result.Succeeded = true; } catch (Exception ex) { result.Messages = ex.Message; result.Succeeded = false; this.CloseStreams(); } return result; } public void WriteData(DbCellValue[][] rows, bool isComplete) { this.EnsureWriterCreated(); foreach (var row in rows) { SetRawObjects(row); writer.WriteRow(row, this.columns); } } private void SetRawObjects(DbCellValue[] row) { for (int i = 0; i < row.Length; i++) { try { // Try to set as the "correct" type var value = Convert.ChangeType(row[i].DisplayValue, columns[i].DataType); row[i].RawObject = value; } catch (Exception) { row[i].RawObject = row[i].DisplayValue; } } } private void EnsureWriterCreated() { if (this.writer == null) { IFileStreamFactory factory; switch (this.requestParams.SaveFormat.ToLowerInvariant()) { case "json": factory = new SaveAsJsonFileStreamFactory() { SaveRequestParams = CreateJsonRequestParams() }; break; case "csv": factory = new SaveAsCsvFileStreamFactory() { SaveRequestParams = CreateCsvRequestParams() }; break; case "markdown": factory = new SaveAsMarkdownFileStreamFactory(CreateMarkdownRequestParams()); break; case "xml": factory = new SaveAsXmlFileStreamFactory() { SaveRequestParams = CreateXmlRequestParams() }; break; case "excel": factory = new SaveAsExcelFileStreamFactory() { SaveRequestParams = CreateExcelRequestParams() }; break; default: throw new Exception(SR.SerializationServiceUnsupportedFormat(this.requestParams.SaveFormat)); } this.writer = factory.GetWriter(requestParams.FilePath, columns); } } public void CloseStreams() { if (this.writer != null) { this.writer.Dispose(); this.writer = null; } } private SaveResultsAsJsonRequestParams CreateJsonRequestParams() { return new SaveResultsAsJsonRequestParams { FilePath = this.requestParams.FilePath, BatchIndex = 0, ResultSetIndex = 0 }; } private SaveResultsAsExcelRequestParams CreateExcelRequestParams() { return new SaveResultsAsExcelRequestParams { FilePath = this.requestParams.FilePath, BatchIndex = 0, ResultSetIndex = 0, IncludeHeaders = this.requestParams.IncludeHeaders }; } private SaveResultsAsCsvRequestParams CreateCsvRequestParams() { return new SaveResultsAsCsvRequestParams { FilePath = this.requestParams.FilePath, BatchIndex = 0, ResultSetIndex = 0, IncludeHeaders = this.requestParams.IncludeHeaders, Delimiter = this.requestParams.Delimiter, LineSeperator = this.requestParams.LineSeparator, TextIdentifier = this.requestParams.TextIdentifier, Encoding = this.requestParams.Encoding, MaxCharsToStore = this.requestParams.MaxCharsToStore }; } private SaveResultsAsMarkdownRequestParams CreateMarkdownRequestParams() => new SaveResultsAsMarkdownRequestParams { FilePath = this.requestParams.FilePath, BatchIndex = 0, ResultSetIndex = 0, IncludeHeaders = this.requestParams.IncludeHeaders, LineSeparator = this.requestParams.LineSeparator, Encoding = this.requestParams.Encoding, }; private SaveResultsAsXmlRequestParams CreateXmlRequestParams() { return new SaveResultsAsXmlRequestParams { FilePath = this.requestParams.FilePath, BatchIndex = 0, ResultSetIndex = 0, Formatted = this.requestParams.Formatted, Encoding = this.requestParams.Encoding }; } } }