// // 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.Data.SqlClient; using System.Threading.Tasks; using Microsoft.SqlTools.Hosting.Protocol; using Microsoft.SqlTools.ServiceLayer.Admin; using Microsoft.SqlTools.ServiceLayer.Admin.Contracts; using Microsoft.SqlTools.ServiceLayer.Connection; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts; using Microsoft.SqlTools.ServiceLayer.Hosting; using Microsoft.SqlTools.ServiceLayer.TaskServices; using System.Threading; using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation; using Microsoft.SqlServer.Management.Smo; using Microsoft.SqlServer.Management.Common; namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery { /// /// Service for Backup and Restore /// public class DisasterRecoveryService { private static readonly Lazy instance = new Lazy(() => new DisasterRecoveryService()); private static ConnectionService connectionService = null; private RestoreDatabaseHelper restoreDatabaseService = new RestoreDatabaseHelper(); /// /// Default, parameterless constructor. /// internal DisasterRecoveryService() { } /// /// Gets the singleton instance object /// public static DisasterRecoveryService Instance { get { return instance.Value; } } /// /// Internal for testing purposes only /// internal static ConnectionService ConnectionServiceInstance { get { if (connectionService == null) { connectionService = ConnectionService.Instance; } return connectionService; } set { connectionService = value; } } /// /// Initializes the service instance /// public void InitializeService(IProtocolEndpoint serviceHost) { // Get database info serviceHost.SetRequestHandler(BackupConfigInfoRequest.Type, HandleBackupConfigInfoRequest); // Create backup serviceHost.SetRequestHandler(BackupRequest.Type, HandleBackupRequest); // Create respore task serviceHost.SetRequestHandler(RestoreRequest.Type, HandleRestoreRequest); // Create respore plan serviceHost.SetRequestHandler(RestorePlanRequest.Type, HandleRestorePlanRequest); } /// /// Handle request to get backup configuration info /// /// /// /// public static async Task HandleBackupConfigInfoRequest( DefaultDatabaseInfoParams optionsParams, RequestContext requestContext) { var response = new BackupConfigInfoResponse(); ConnectionInfo connInfo; DisasterRecoveryService.ConnectionServiceInstance.TryFindConnection( optionsParams.OwnerUri, out connInfo); if (connInfo != null) { DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(connInfo, databaseExists: true); SqlConnection sqlConn = GetSqlConnection(connInfo); if ((sqlConn != null) && !connInfo.IsSqlDW && !connInfo.IsAzure) { BackupConfigInfo backupConfigInfo = DisasterRecoveryService.Instance.GetBackupConfigInfo(helper.DataContainer, sqlConn, sqlConn.Database); backupConfigInfo.DatabaseInfo = AdminService.GetDatabaseInfo(connInfo); response.BackupConfigInfo = backupConfigInfo; } } await requestContext.SendResult(response); } /// /// Handles a restore request /// internal async Task HandleRestorePlanRequest( RestoreParams restoreParams, RequestContext requestContext) { RestorePlanResponse response = new RestorePlanResponse(); ConnectionInfo connInfo; bool supported = IsBackupRestoreOperationSupported(restoreParams, out connInfo); if (supported && connInfo != null) { RestoreDatabaseTaskDataObject restoreDataObject = this.restoreDatabaseService.CreateRestoreDatabaseTaskDataObject(restoreParams); response = this.restoreDatabaseService.CreateRestorePlanResponse(restoreDataObject); } else { response.CanRestore = false; response.ErrorMessage = "Restore is not supported"; //TOOD: have a better error message } await requestContext.SendResult(response); } /// /// Handles a restore request /// internal async Task HandleRestoreRequest( RestoreParams restoreParams, RequestContext requestContext) { RestoreResponse response = new RestoreResponse(); ConnectionInfo connInfo; bool supported = IsBackupRestoreOperationSupported(restoreParams, out connInfo); if (supported && connInfo != null) { try { RestoreDatabaseTaskDataObject restoreDataObject = this.restoreDatabaseService.CreateRestoreDatabaseTaskDataObject(restoreParams); if (restoreDataObject != null) { // create task metadata TaskMetadata metadata = new TaskMetadata(); metadata.ServerName = connInfo.ConnectionDetails.ServerName; metadata.DatabaseName = connInfo.ConnectionDetails.DatabaseName; metadata.Name = SR.Backup_TaskName; metadata.IsCancelable = true; metadata.Data = restoreDataObject; // create restore task and perform SqlTask sqlTask = SqlTaskManager.Instance.CreateAndRun(metadata, this.restoreDatabaseService.RestoreTaskAsync, restoreDatabaseService.CancelTaskAsync); response.TaskId = sqlTask.TaskId.ToString(); } else { response.ErrorMessage = "Failed to create restore task"; } } catch (Exception ex) { response.ErrorMessage = ex.Message; } } else { response.ErrorMessage = "Restore database is not supported"; //TOOD: have a better error message } await requestContext.SendResult(response); } /// /// Handles a backup request /// internal static async Task HandleBackupRequest( BackupParams backupParams, RequestContext requestContext) { ConnectionInfo connInfo; DisasterRecoveryService.ConnectionServiceInstance.TryFindConnection( backupParams.OwnerUri, out connInfo); if (connInfo != null) { DatabaseTaskHelper helper = AdminService.CreateDatabaseTaskHelper(connInfo, databaseExists: true); SqlConnection sqlConn = GetSqlConnection(connInfo); if ((sqlConn != null) && !connInfo.IsSqlDW && !connInfo.IsAzure) { BackupOperation backupOperation = DisasterRecoveryService.Instance.SetBackupInput(helper.DataContainer, sqlConn, backupParams.BackupInfo); // create task metadata TaskMetadata metadata = new TaskMetadata(); metadata.ServerName = connInfo.ConnectionDetails.ServerName; metadata.DatabaseName = connInfo.ConnectionDetails.DatabaseName; metadata.Name = SR.Backup_TaskName; metadata.IsCancelable = true; metadata.Data = backupOperation; // create backup task and perform SqlTask sqlTask = SqlTaskManager.Instance.CreateTask(metadata, Instance.BackupTaskAsync); sqlTask.Run(); } } await requestContext.SendResult(new BackupResponse()); } internal static SqlConnection GetSqlConnection(ConnectionInfo connInfo) { try { // increase the connection timeout to at least 30 seconds and and build connection string // enable PersistSecurityInfo to handle issues in SMO where the connection context is lost in reconnections int? originalTimeout = connInfo.ConnectionDetails.ConnectTimeout; bool? originalPersistSecurityInfo = connInfo.ConnectionDetails.PersistSecurityInfo; connInfo.ConnectionDetails.ConnectTimeout = Math.Max(30, originalTimeout ?? 0); connInfo.ConnectionDetails.PersistSecurityInfo = true; string connectionString = ConnectionService.BuildConnectionString(connInfo.ConnectionDetails); connInfo.ConnectionDetails.ConnectTimeout = originalTimeout; connInfo.ConnectionDetails.PersistSecurityInfo = originalPersistSecurityInfo; // open a dedicated binding server connection SqlConnection sqlConn = new SqlConnection(connectionString); sqlConn.Open(); return sqlConn; } catch (Exception) { } return null; } private bool IsBackupRestoreOperationSupported(RestoreParams restoreParams, out ConnectionInfo connectionInfo) { SqlConnection sqlConn = null; try { ConnectionInfo connInfo; DisasterRecoveryService.ConnectionServiceInstance.TryFindConnection( restoreParams.OwnerUri, out connInfo); if (connInfo != null) { sqlConn = GetSqlConnection(connInfo); if ((sqlConn != null) && !connInfo.IsSqlDW && !connInfo.IsAzure) { connectionInfo = connInfo; return true; } } } catch { if(sqlConn != null) { sqlConn.Close(); } } connectionInfo = null; return false; } internal BackupConfigInfo GetBackupConfigInfo(CDataContainer dataContainer, SqlConnection sqlConnection, string databaseName) { BackupOperation backupOperation = new BackupOperation(); backupOperation.Initialize(dataContainer, sqlConnection); return backupOperation.GetBackupConfigInfo(databaseName); } internal BackupOperation SetBackupInput(CDataContainer dataContainer, SqlConnection sqlConnection, BackupInfo input) { BackupOperation backupOperation = new BackupOperation(); backupOperation.Initialize(dataContainer, sqlConnection); backupOperation.SetBackupInput(input); return backupOperation; } /// /// For testing purpose only /// internal void PerformBackup(BackupOperation backupOperation) { backupOperation.PerformBackup(); } /// /// Create a backup task for execution and cancellation /// /// /// internal async Task BackupTaskAsync(SqlTask sqlTask) { sqlTask.AddMessage(SR.Task_InProgress, SqlTaskStatus.InProgress, true); IBackupOperation backupOperation = sqlTask.TaskMetadata.Data as IBackupOperation; TaskResult taskResult = null; if (backupOperation != null) { AutoResetEvent backupCompletedEvent = new AutoResetEvent(initialState: false); Task performTask = PerformTaskAsync(backupOperation); Task cancelTask = CancelTaskAsync(backupOperation, sqlTask.TokenSource.Token, backupCompletedEvent); Task completedTask = await Task.WhenAny(performTask, cancelTask); // Release the cancelTask if (completedTask == performTask) { backupCompletedEvent.Set(); } sqlTask.AddMessage(completedTask.Result.TaskStatus == SqlTaskStatus.Failed ? completedTask.Result.ErrorMessage : SR.Task_Completed, completedTask.Result.TaskStatus); taskResult = completedTask.Result; } else { taskResult = new TaskResult(); taskResult.TaskStatus = SqlTaskStatus.Failed; } return taskResult; } /// /// Async task to execute backup /// /// /// private async Task PerformTaskAsync(IBackupOperation backupOperation) { // Create a task to perform backup return await Task.Factory.StartNew(() => { TaskResult result = new TaskResult(); try { backupOperation.PerformBackup(); result.TaskStatus = SqlTaskStatus.Succeeded; } catch (Exception ex) { result.TaskStatus = SqlTaskStatus.Failed; result.ErrorMessage = ex.Message; if (ex.InnerException != null) { result.ErrorMessage += System.Environment.NewLine + ex.InnerException.Message; } } return result; }); } /// /// Async task to cancel backup /// /// /// /// /// private async Task CancelTaskAsync(IBackupOperation backupOperation, CancellationToken token, AutoResetEvent backupCompletedEvent) { // Create a task for backup cancellation request return await Task.Factory.StartNew(() => { TaskResult result = new TaskResult(); WaitHandle[] waitHandles = new WaitHandle[2] { backupCompletedEvent, token.WaitHandle }; WaitHandle.WaitAny(waitHandles); try { backupOperation.CancelBackup(); result.TaskStatus = SqlTaskStatus.Canceled; } catch (Exception ex) { result.TaskStatus = SqlTaskStatus.Failed; result.ErrorMessage = ex.Message; } return result; }); } } }