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