mirror of
https://github.com/ckaczor/sqltoolsservice.git
synced 2026-01-14 01:25:40 -05:00
Feature/restore db (#403)
* Added service handlers for restore database operations
This commit is contained in:
Binary file not shown.
BIN
bin/nuget/Microsoft.SqlServer.Smo.140.2.2.nupkg
Normal file
BIN
bin/nuget/Microsoft.SqlServer.Smo.140.2.2.nupkg
Normal file
Binary file not shown.
@@ -19,7 +19,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.4.0-preview1-25305-02" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.1" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.2" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Management.SqlScriptPublishModel" Version="140.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.0.0-preview1-002111" />
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.4.0-preview1-25305-02" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.1" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.2" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Management.SqlScriptPublishModel" Version="140.2.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="10.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.0.0-preview1-002111" />
|
||||
|
||||
@@ -340,28 +340,6 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDestinationPathValid(string path, ref bool isFolder)
|
||||
{
|
||||
Enumerator en = null;
|
||||
DataTable dt;
|
||||
Request req = new Request();
|
||||
|
||||
en = new Enumerator();
|
||||
req.Urn = "Server/File[@FullName='" + Urn.EscapeString(path) + "']";
|
||||
dt = en.Process(this.sqlConnection, req);
|
||||
|
||||
if (dt.Rows.Count > 0)
|
||||
{
|
||||
isFolder = !(Convert.ToBoolean(dt.Rows[0]["IsFile"], System.Globalization.CultureInfo.InvariantCulture));
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isFolder = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetMediaNameFromBackupSetId(int backupSetId)
|
||||
{
|
||||
Enumerator en = null;
|
||||
@@ -422,72 +400,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery
|
||||
return result;
|
||||
}
|
||||
|
||||
public string GetNewPhysicalRestoredFileName(string filePathParam, string dbName, bool isNewDatabase, string type, ref int fileIndex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePathParam))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string result = string.Empty;
|
||||
string filePath = filePathParam;
|
||||
int idx = filePath.LastIndexOf('\\');
|
||||
string folderPath = filePath.Substring(0,idx);
|
||||
|
||||
string fileName = filePath.Substring(idx + 1);
|
||||
idx = fileName.LastIndexOf('.');
|
||||
string fileExtension = fileName.Substring(idx + 1);
|
||||
|
||||
bool isFolder = true;
|
||||
bool isValidPath = IsDestinationPathValid(folderPath, ref isFolder);
|
||||
|
||||
if (!isValidPath || !isFolder)
|
||||
{
|
||||
SMO.Server server = new SMO.Server(this.sqlConnection);
|
||||
if (type != RestoreConstants.Log)
|
||||
{
|
||||
folderPath = server.Settings.DefaultFile;
|
||||
if (folderPath.Length == 0)
|
||||
{
|
||||
folderPath = server.Information.MasterDBPath;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
folderPath = server.Settings.DefaultLog;
|
||||
if (folderPath.Length == 0)
|
||||
{
|
||||
folderPath = server.Information.MasterDBLogPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isNewDatabase)
|
||||
{
|
||||
return filePathParam;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNewDatabase)
|
||||
{
|
||||
result = folderPath + "\\" + dbName + "." + fileExtension;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (0 != string.Compare(fileExtension, "mdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = folderPath + "\\" + dbName + "_" + Convert.ToString(fileIndex, System.Globalization.CultureInfo.InvariantCulture) + "." + fileExtension;
|
||||
fileIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = folderPath + "\\" + dbName + "." + fileExtension;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// TODO: This is implemented as internal property in SMO.
|
||||
public bool IsLocalPrimaryReplica(string databaseName)
|
||||
@@ -663,29 +576,29 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery
|
||||
return dict;
|
||||
}*/
|
||||
|
||||
public void GetBackupSetTypeAndComponent(int numType, ref string backupType, ref string backupComponent)
|
||||
public static void GetBackupSetTypeAndComponent(BackupSetType backupSetType, out string backupType, out string backupComponent)
|
||||
{
|
||||
switch (numType)
|
||||
switch (backupSetType)
|
||||
{
|
||||
case 1:
|
||||
case BackupSetType.Database:
|
||||
backupType = RestoreConstants.TypeFull;
|
||||
backupComponent = RestoreConstants.ComponentDatabase;
|
||||
break;
|
||||
case 2:
|
||||
case BackupSetType.Differential:
|
||||
backupType = RestoreConstants.TypeTransactionLog;
|
||||
backupComponent = "";
|
||||
backupComponent = RestoreConstants.ComponentDatabase;
|
||||
break;
|
||||
case 4:
|
||||
case BackupSetType.FileOrFileGroup:
|
||||
backupType = RestoreConstants.TypeFilegroup;
|
||||
backupComponent = RestoreConstants.ComponentFile;
|
||||
break;
|
||||
case 5:
|
||||
case BackupSetType.FileOrFileGroupDifferential:
|
||||
backupType = RestoreConstants.TypeDifferential;
|
||||
backupComponent = RestoreConstants.ComponentDatabase;
|
||||
break;
|
||||
case 6:
|
||||
backupType = RestoreConstants.TypeFilegroupDifferential;
|
||||
backupComponent = RestoreConstants.ComponentFile;
|
||||
case BackupSetType.Log:
|
||||
backupType = RestoreConstants.Log;
|
||||
backupComponent = RestoreConstants.ComponentLog;
|
||||
break;
|
||||
default:
|
||||
backupType = RestoreConstants.NotKnown;
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Restore request parameters
|
||||
/// </summary>
|
||||
public class RestoreParams
|
||||
{
|
||||
/// <summary>
|
||||
/// The Uri to find the connection to do the restore operations
|
||||
/// </summary>
|
||||
public string OwnerUri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The backup file path
|
||||
/// </summary>
|
||||
public string BackupFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Database name to restore from (either the back file path or database name can be used for restore operation,
|
||||
/// If the backup file is set, the database name will be ignored)
|
||||
/// </summary>
|
||||
public string DatabaseName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set to true, the db files will be relocated to default data location in the server
|
||||
/// </summary>
|
||||
public bool RelocateDbFiles { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore response
|
||||
/// </summary>
|
||||
public class RestoreResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates if the restore task created successfully
|
||||
/// </summary>
|
||||
public bool Result { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The task id assosiated witht the restore operation
|
||||
/// </summary>
|
||||
public string TaskId { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Errors occurred while creating the restore operation task
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restore Plan Response
|
||||
/// </summary>
|
||||
public class RestorePlanResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// The backup file path
|
||||
/// </summary>
|
||||
public string BackupFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the restore operation is supported
|
||||
/// </summary>
|
||||
public bool CanRestore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Errors occurred while creating restore plan
|
||||
/// </summary>
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The db files included in the backup file
|
||||
/// </summary>
|
||||
public IEnumerable<string> DbFiles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Server name
|
||||
/// </summary>
|
||||
public string ServerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Database name to restore to
|
||||
/// </summary>
|
||||
public string DatabaseName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether relocating the db files is required
|
||||
/// because the original file paths are not valid in the target server
|
||||
/// </summary>
|
||||
public bool RelocateFilesNeeded { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default Data folder path in the target server
|
||||
/// </summary>
|
||||
public string DefaultDataFolder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Default log folder path in the target server
|
||||
/// </summary>
|
||||
public string DefaultLogFolder { get; set; }
|
||||
}
|
||||
|
||||
public class RestoreRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<RestoreParams, RestoreResponse> Type =
|
||||
RequestType<RestoreParams, RestoreResponse>.Create("disasterrecovery/restore");
|
||||
}
|
||||
|
||||
public class RestorePlanRequest
|
||||
{
|
||||
public static readonly
|
||||
RequestType<RestoreParams, RestorePlanResponse> Type =
|
||||
RequestType<RestoreParams, RestorePlanResponse>.Create("disasterrecovery/restoreplan");
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery
|
||||
public static string TypeFilegroupDifferential = "Filegroup Differential";
|
||||
public static string ComponentDatabase = "Database";
|
||||
public static string ComponentFile = "File";
|
||||
public static string ComponentLog = "Log";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ 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
|
||||
{
|
||||
@@ -23,6 +26,7 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery
|
||||
{
|
||||
private static readonly Lazy<DisasterRecoveryService> instance = new Lazy<DisasterRecoveryService>(() => new DisasterRecoveryService());
|
||||
private static ConnectionService connectionService = null;
|
||||
private RestoreDatabaseHelper restoreDatabaseService = new RestoreDatabaseHelper();
|
||||
|
||||
/// <summary>
|
||||
/// Default, parameterless constructor.
|
||||
@@ -61,12 +65,17 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery
|
||||
/// <summary>
|
||||
/// Initializes the service instance
|
||||
/// </summary>
|
||||
public void InitializeService(ServiceHost serviceHost)
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -100,6 +109,81 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery
|
||||
await requestContext.SendResult(response);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a restore request
|
||||
/// </summary>
|
||||
internal async Task HandleRestorePlanRequest(
|
||||
RestoreParams restoreParams,
|
||||
RequestContext<RestorePlanResponse> 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);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a restore request
|
||||
/// </summary>
|
||||
internal async Task HandleRestoreRequest(
|
||||
RestoreParams restoreParams,
|
||||
RequestContext<RestoreResponse> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles a backup request
|
||||
/// </summary>
|
||||
@@ -163,6 +247,37 @@ namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Backup set Information
|
||||
/// </summary>
|
||||
public class BackupSetInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Backup type (Full, Transaction Log, Differential ...)
|
||||
/// </summary>
|
||||
public string BackupType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Backup component (Database, File, Log ...)
|
||||
/// </summary>
|
||||
public string BackupComponent { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlServer.Management.Common;
|
||||
using Microsoft.SqlServer.Management.Smo;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||
using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.TaskServices;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Includes method to all restore operations
|
||||
/// </summary>
|
||||
public class RestoreDatabaseHelper
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Create a backup task for execution and cancellation
|
||||
/// </summary>
|
||||
/// <param name="sqlTask"></param>
|
||||
/// <returns></returns>
|
||||
internal async Task<TaskResult> RestoreTaskAsync(SqlTask sqlTask)
|
||||
{
|
||||
sqlTask.AddMessage(SR.Task_InProgress, SqlTaskStatus.InProgress, true);
|
||||
RestoreDatabaseTaskDataObject restoreDataObject = sqlTask.TaskMetadata.Data as RestoreDatabaseTaskDataObject;
|
||||
TaskResult taskResult = null;
|
||||
|
||||
if (restoreDataObject != null)
|
||||
{
|
||||
// Create a task to perform backup
|
||||
return await Task.Factory.StartNew(() =>
|
||||
{
|
||||
TaskResult result = new TaskResult();
|
||||
try
|
||||
{
|
||||
ExecuteRestore(restoreDataObject);
|
||||
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;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
taskResult = new TaskResult();
|
||||
taskResult.TaskStatus = SqlTaskStatus.Failed;
|
||||
}
|
||||
|
||||
return taskResult;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Async task to cancel restore
|
||||
/// </summary>
|
||||
public async Task<TaskResult> CancelTaskAsync(SqlTask sqlTask)
|
||||
{
|
||||
RestoreDatabaseTaskDataObject restoreDataObject = sqlTask.TaskMetadata.Data as RestoreDatabaseTaskDataObject;
|
||||
TaskResult taskResult = null;
|
||||
|
||||
|
||||
if (restoreDataObject != null && restoreDataObject.IsValid)
|
||||
{
|
||||
// Create a task for backup cancellation request
|
||||
return await Task.Factory.StartNew(() =>
|
||||
{
|
||||
|
||||
foreach (Restore restore in restoreDataObject.RestorePlan.RestoreOperations)
|
||||
{
|
||||
restore.Abort();
|
||||
}
|
||||
|
||||
|
||||
return new TaskResult
|
||||
{
|
||||
TaskStatus = SqlTaskStatus.Canceled
|
||||
};
|
||||
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
taskResult = new TaskResult();
|
||||
taskResult.TaskStatus = SqlTaskStatus.Failed;
|
||||
}
|
||||
|
||||
return taskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a restore plan, The result includes the information about the backup set,
|
||||
/// the files and the database to restore to
|
||||
/// </summary>
|
||||
/// <param name="requestParam">Restore request</param>s
|
||||
/// <returns>Restore plan</returns>
|
||||
public RestorePlanResponse CreateRestorePlanResponse(RestoreDatabaseTaskDataObject restoreDataObject)
|
||||
{
|
||||
RestorePlanResponse response = new RestorePlanResponse()
|
||||
{
|
||||
DatabaseName = restoreDataObject.RestoreParams.DatabaseName
|
||||
};
|
||||
if (restoreDataObject != null && restoreDataObject.IsValid)
|
||||
{
|
||||
UpdateRestorePlan(restoreDataObject);
|
||||
|
||||
if (restoreDataObject != null && restoreDataObject.IsValid)
|
||||
{
|
||||
response.DatabaseName = restoreDataObject.RestorePlanner.DatabaseName;
|
||||
response.DbFiles = restoreDataObject.DbFiles.Select(x => x.PhysicalName);
|
||||
response.CanRestore = CanRestore(restoreDataObject);
|
||||
|
||||
if (!response.CanRestore)
|
||||
{
|
||||
response.ErrorMessage = "Backup not supported.";
|
||||
}
|
||||
|
||||
response.RelocateFilesNeeded = !restoreDataObject.DbFilesLocationAreValid();
|
||||
response.DefaultDataFolder = restoreDataObject.DefaultDataFileFolder;
|
||||
response.DefaultLogFolder = restoreDataObject.DefaultLogFileFolder;
|
||||
}
|
||||
else
|
||||
{
|
||||
response.ErrorMessage = "Failed to create restore plan";
|
||||
response.CanRestore = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
response.ErrorMessage = "Failed to create restore database plan";
|
||||
}
|
||||
return response;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the restoring the restoreDataObject is supported in the service
|
||||
/// </summary>
|
||||
private static bool CanRestore(RestoreDatabaseTaskDataObject restoreDataObject)
|
||||
{
|
||||
if (restoreDataObject != null)
|
||||
{
|
||||
var backupTypes = restoreDataObject.GetBackupSetInfo();
|
||||
return backupTypes.Any(x => x.BackupType.StartsWith(RestoreConstants.TypeFull));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates anew restore task object to do the restore operations
|
||||
/// </summary>
|
||||
/// <param name="restoreParams">Restore request parameters</param>
|
||||
/// <returns>Restore task object</returns>
|
||||
public RestoreDatabaseTaskDataObject CreateRestoreDatabaseTaskDataObject(RestoreParams restoreParams)
|
||||
{
|
||||
ConnectionInfo connInfo;
|
||||
DisasterRecoveryService.ConnectionServiceInstance.TryFindConnection(
|
||||
restoreParams.OwnerUri,
|
||||
out connInfo);
|
||||
|
||||
if (connInfo != null)
|
||||
{
|
||||
Server server = new Server(new ServerConnection(connInfo.ConnectionDetails.ServerName));
|
||||
|
||||
RestoreDatabaseTaskDataObject restoreDataObject = new RestoreDatabaseTaskDataObject(server, restoreParams.DatabaseName);
|
||||
restoreDataObject.RestoreParams = restoreParams;
|
||||
return restoreDataObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a restore data object that includes the plan to do the restore operation
|
||||
/// </summary>
|
||||
/// <param name="requestParam"></param>
|
||||
/// <returns></returns>
|
||||
private void UpdateRestorePlan(RestoreDatabaseTaskDataObject restoreDataObject)
|
||||
{
|
||||
// Server server = new Server(new ServerConnection(connInfo.ConnectionDetails.ServerName));
|
||||
//RestoreDatabaseTaskDataObject restoreDataObject = new RestoreDatabaseTaskDataObject(server, requestParam.DatabaseName);
|
||||
if (!string.IsNullOrEmpty(restoreDataObject.RestoreParams.BackupFilePath))
|
||||
{
|
||||
restoreDataObject.AddFile(restoreDataObject.RestoreParams.BackupFilePath);
|
||||
}
|
||||
restoreDataObject.RestorePlanner.ReadHeaderFromMedia = !string.IsNullOrEmpty(restoreDataObject.RestoreParams.BackupFilePath);
|
||||
var dbNames = restoreDataObject.GetSourceDbNames();
|
||||
string dbName = dbNames.First();
|
||||
restoreDataObject.RestorePlanner.DatabaseName = dbName;
|
||||
restoreDataObject.UpdateRestorePlan(restoreDataObject.RestoreParams.RelocateDbFiles);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the restore operation
|
||||
/// </summary>
|
||||
/// <param name="requestParam"></param>
|
||||
public void ExecuteRestore(RestoreDatabaseTaskDataObject restoreDataObject)
|
||||
{
|
||||
UpdateRestorePlan(restoreDataObject);
|
||||
|
||||
if (restoreDataObject != null && CanRestore(restoreDataObject))
|
||||
{
|
||||
restoreDataObject.RestorePlan.Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,816 @@
|
||||
//
|
||||
// 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.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Microsoft.SqlServer.Management.Smo;
|
||||
using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation
|
||||
{
|
||||
/// <summary>
|
||||
/// Includes the plan with all the data required to do a restore operation on server
|
||||
/// </summary>
|
||||
public class RestoreDatabaseTaskDataObject
|
||||
{
|
||||
public RestoreDatabaseTaskDataObject(Server server, String databaseName)
|
||||
{
|
||||
this.Server = server;
|
||||
this.Util = new RestoreUtil(server);
|
||||
restorePlanner = new DatabaseRestorePlanner(server);
|
||||
|
||||
if (String.IsNullOrEmpty(databaseName))
|
||||
{
|
||||
this.restorePlanner = new DatabaseRestorePlanner(server);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.restorePlanner = new DatabaseRestorePlanner(server, databaseName);
|
||||
this.targetDbName = databaseName;
|
||||
}
|
||||
|
||||
this.restorePlanner.TailLogBackupFile = this.Util.GetDefaultTailLogbackupFile(databaseName);
|
||||
this.restoreOptions = new RestoreOptions();
|
||||
//the server will send events in intervals of 5 percent
|
||||
this.restoreOptions.PercentCompleteNotification = 5;
|
||||
}
|
||||
|
||||
public bool IsValid
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.Server != null && this.RestorePlanner != null;
|
||||
}
|
||||
}
|
||||
|
||||
public RestoreParams RestoreParams { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Database names includes in the restore plan
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public List<String> GetSourceDbNames()
|
||||
{
|
||||
return Util.GetSourceDbNames(this.restorePlanner.BackupMediaList, this.CredentialName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current sqlserver instance
|
||||
/// </summary>
|
||||
public Server Server;
|
||||
|
||||
/// <summary>
|
||||
/// Recent exception that was thrown
|
||||
/// Displayed at the top of the dialog
|
||||
/// </summary>
|
||||
public Exception ActiveException { get; set; }
|
||||
|
||||
public Exception CreateOrUpdateRestorePlanException { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Add a backup file to restore plan media list
|
||||
/// </summary>
|
||||
/// <param name="filePath"></param>
|
||||
public void AddFile(string filePath)
|
||||
{
|
||||
this.RestorePlanner.BackupMediaList.Add(new BackupDeviceItem
|
||||
{
|
||||
DeviceType = DeviceType.File,
|
||||
Name = filePath
|
||||
});
|
||||
}
|
||||
|
||||
public RestoreUtil Util { get; set; }
|
||||
|
||||
private DatabaseRestorePlanner restorePlanner;
|
||||
|
||||
/// <summary>
|
||||
/// SMO database restore planner used to create a restore plan
|
||||
/// </summary>
|
||||
public DatabaseRestorePlanner RestorePlanner
|
||||
{
|
||||
get { return restorePlanner; }
|
||||
}
|
||||
|
||||
private string tailLogBackupFile;
|
||||
private bool planUpdateRequired = false;
|
||||
|
||||
/// <summary>
|
||||
/// File to backup tail log before doing the restore
|
||||
/// </summary>
|
||||
public string TailLogBackupFile
|
||||
{
|
||||
get { return tailLogBackupFile; }
|
||||
set
|
||||
{
|
||||
if (tailLogBackupFile == null || !tailLogBackupFile.Equals(value))
|
||||
{
|
||||
this.RestorePlanner.TailLogBackupFile = value;
|
||||
this.planUpdateRequired = true;
|
||||
this.tailLogBackupFile = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RestoreOptions restoreOptions;
|
||||
|
||||
public RestoreOptions RestoreOptions
|
||||
{
|
||||
get { return restoreOptions; }
|
||||
}
|
||||
|
||||
private string dataFilesFolder = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Folder for all data files when relocate all files option is used
|
||||
/// </summary>
|
||||
public string DataFilesFolder
|
||||
{
|
||||
get { return this.dataFilesFolder; }
|
||||
set
|
||||
{
|
||||
if (this.dataFilesFolder == null || !this.dataFilesFolder.Equals(value))
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri pathUri;
|
||||
bool fUriCreated = Uri.TryCreate(value, UriKind.Absolute, out pathUri);
|
||||
|
||||
if (fUriCreated && pathUri.Scheme == "https")
|
||||
{
|
||||
this.dataFilesFolder = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.dataFilesFolder = PathWrapper.GetDirectoryName(value);
|
||||
}
|
||||
if (string.IsNullOrEmpty(this.dataFilesFolder))
|
||||
{
|
||||
this.dataFilesFolder = this.Server.DefaultFile;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.ActiveException = ex;
|
||||
}
|
||||
|
||||
this.RelocateDbFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string logFilesFolder = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Folder for all log files when relocate all files option is used
|
||||
/// </summary>
|
||||
public string LogFilesFolder
|
||||
{
|
||||
get { return this.logFilesFolder; }
|
||||
set
|
||||
{
|
||||
if (this.logFilesFolder == null || !this.logFilesFolder.Equals(value))
|
||||
{
|
||||
try
|
||||
{
|
||||
Uri pathUri;
|
||||
bool fUriCreated = Uri.TryCreate(value, UriKind.Absolute, out pathUri);
|
||||
|
||||
if (fUriCreated && pathUri.Scheme == "https")
|
||||
{
|
||||
this.logFilesFolder = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.logFilesFolder = PathWrapper.GetDirectoryName(value);
|
||||
}
|
||||
if (string.IsNullOrEmpty(this.logFilesFolder))
|
||||
{
|
||||
this.logFilesFolder = Server.DefaultLog;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.ActiveException = ex;
|
||||
}
|
||||
this.RelocateDbFiles();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [prompt before each backup].
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [prompt before each backup]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool PromptBeforeEachBackup { get; set; }
|
||||
|
||||
private void RelocateDbFiles()
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (DbFile dbFile in this.DbFiles)
|
||||
{
|
||||
string fileName = this.GetTargetDbFilePhysicalName(dbFile.PhysicalName);
|
||||
if (!dbFile.DbFileType.Equals("Log"))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this.dataFilesFolder))
|
||||
{
|
||||
dbFile.PhysicalNameRelocate = PathWrapper.Combine(this.dataFilesFolder, fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
dbFile.PhysicalNameRelocate = fileName;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this.logFilesFolder))
|
||||
{
|
||||
dbFile.PhysicalNameRelocate = PathWrapper.Combine(this.logFilesFolder, fileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
dbFile.PhysicalNameRelocate = fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.ActiveException = ex;
|
||||
}
|
||||
}
|
||||
|
||||
private List<DbFile> dbFiles = new List<DbFile>();
|
||||
|
||||
/// <summary>
|
||||
/// List of files of the source database or in the backup file
|
||||
/// </summary>
|
||||
public List<DbFile> DbFiles
|
||||
{
|
||||
get { return dbFiles; }
|
||||
}
|
||||
|
||||
internal RestorePlan restorePlan;
|
||||
|
||||
/// <summary>
|
||||
/// Restore plan to do the restore
|
||||
/// </summary>
|
||||
public RestorePlan RestorePlan
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.restorePlan == null)
|
||||
{
|
||||
this.UpdateRestorePlan(false);
|
||||
}
|
||||
return this.restorePlan;
|
||||
}
|
||||
internal set
|
||||
{
|
||||
this.restorePlan = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool[] RestoreSelected;
|
||||
|
||||
/// <summary>
|
||||
/// The database being restored
|
||||
/// </summary>
|
||||
public string targetDbName = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The database used to restore from
|
||||
/// </summary>
|
||||
public string sourceDbName = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [close existing connections].
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// <c>true</c> if [close existing connections]; otherwise, <c>false</c>.
|
||||
/// </value>
|
||||
public bool CloseExistingConnections { get; set; }
|
||||
|
||||
/*
|
||||
private BackupTimeLine.TimeLineDuration timeLineDuration = BackupTimeLine.TimeLineDuration.Day;
|
||||
|
||||
public BackupTimeLine.TimeLineDuration TimeLineDuration
|
||||
{
|
||||
get { return this.timeLineDuration; }
|
||||
set { this.timeLineDuration = value; }
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Sql server credential name used to restore from Microsoft Azure url
|
||||
/// </summary>
|
||||
internal string CredentialName = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Azure container SAS policy
|
||||
/// </summary>
|
||||
internal string ContainerSharedAccessPolicy = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets RestorePlan to perform restore and to script
|
||||
/// </summary>
|
||||
public RestorePlan GetRestorePlanForExecutionAndScript()
|
||||
{
|
||||
this.ActiveException = null; //Clear any existing exceptions as the plan is getting recreated.
|
||||
//Clear any existing exceptions as new plan is getting recreated.
|
||||
this.CreateOrUpdateRestorePlanException = null;
|
||||
bool tailLogBackup = this.RestorePlanner.BackupTailLog;
|
||||
if (this.planUpdateRequired)
|
||||
{
|
||||
this.RestorePlan = this.RestorePlanner.CreateRestorePlan(this.RestoreOptions);
|
||||
this.UpdateRestoreSelected();
|
||||
this.Util.AddCredentialNameForUrlBackupSet(this.RestorePlan, this.CredentialName);
|
||||
}
|
||||
RestorePlan rp = new RestorePlan(this.Server);
|
||||
rp.RestoreAction = RestoreActionType.Database;
|
||||
if (this.RestorePlan != null)
|
||||
{
|
||||
if (this.RestorePlan.TailLogBackupOperation != null && tailLogBackup)
|
||||
{
|
||||
rp.TailLogBackupOperation = this.RestorePlan.TailLogBackupOperation;
|
||||
}
|
||||
int i = 0;
|
||||
foreach (Restore res in this.RestorePlan.RestoreOperations)
|
||||
{
|
||||
if (this.RestoreSelected[i] == true)
|
||||
{
|
||||
rp.RestoreOperations.Add(res);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
this.SetRestorePlanProperties(rp);
|
||||
return rp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the RestoreSelected Array to hold information about updated Restore Plan
|
||||
/// </summary>
|
||||
private void UpdateRestoreSelected()
|
||||
{
|
||||
int operationsCount = this.RestorePlan.RestoreOperations.Count;
|
||||
// The given condition will return true only if new backup has been added on database during lifetime of restore dialog.
|
||||
// This will happen when tail log backup is taken successfully and subsequent restores have failed.
|
||||
if (operationsCount > this.RestoreSelected.Length)
|
||||
{
|
||||
bool[] tempRestoreSel = new bool[this.RestorePlan.RestoreOperations.Count];
|
||||
for (int i = 0; i < operationsCount; i++)
|
||||
{
|
||||
if (i < RestoreSelected.Length)
|
||||
{
|
||||
//Retain all the old values.
|
||||
tempRestoreSel[i] = RestoreSelected[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
//Do not add the newly added backupset into Restore plan by default.
|
||||
tempRestoreSel[i] = false;
|
||||
}
|
||||
}
|
||||
this.RestoreSelected = tempRestoreSel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the physical name for the target Db file.
|
||||
/// It is the sourceDbName replaced with targetDbName in sourceFilename.
|
||||
/// If either sourceDbName or TargetDbName is empty, the source Db filename is returned.
|
||||
/// </summary>
|
||||
/// <param name="sourceDbFilePhysicalLocation">source DbFile physical location</param>
|
||||
/// <returns></returns>
|
||||
private string GetTargetDbFilePhysicalName(string sourceDbFilePhysicalLocation)
|
||||
{
|
||||
string fileName = Path.GetFileName(sourceDbFilePhysicalLocation);
|
||||
if (!string.IsNullOrEmpty(this.sourceDbName) && !string.IsNullOrEmpty(this.targetDbName))
|
||||
{
|
||||
string sourceFilename = fileName;
|
||||
fileName = sourceFilename.Replace(this.sourceDbName, this.targetDbName);
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public IEnumerable<BackupSetInfo> GetBackupSetInfo()
|
||||
{
|
||||
List<BackupSetInfo> result = new List<BackupSetInfo>();
|
||||
foreach (Restore restore in RestorePlan.RestoreOperations)
|
||||
{
|
||||
BackupSet backupSet = restore.BackupSet;
|
||||
|
||||
String bkSetComponent;
|
||||
String bkSetType;
|
||||
CommonUtilities.GetBackupSetTypeAndComponent(backupSet.BackupSetType, out bkSetType, out bkSetComponent);
|
||||
|
||||
if (this.Server.Version.Major > 8 && backupSet.IsCopyOnly)
|
||||
{
|
||||
bkSetType += " (Copy Only)";
|
||||
}
|
||||
result.Add(new BackupSetInfo { BackupComponent = bkSetComponent, BackupType = bkSetType });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the files of the database
|
||||
/// </summary>
|
||||
public List<DbFile> GetDbFiles()
|
||||
{
|
||||
Database db = null;
|
||||
List<DbFile> ret = new List<DbFile>();
|
||||
if (!this.RestorePlanner.ReadHeaderFromMedia)
|
||||
{
|
||||
db = this.Server.Databases[this.RestorePlanner.DatabaseName];
|
||||
}
|
||||
if (restorePlan != null && restorePlan.RestoreOperations.Count > 0)
|
||||
{
|
||||
if (db != null && db.Status == DatabaseStatus.Normal)
|
||||
{
|
||||
ret = this.Util.GetDbFiles(db);
|
||||
}
|
||||
else
|
||||
{
|
||||
ret = this.Util.GetDbFiles(restorePlan.RestoreOperations[0]);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public string DefaultDataFileFolder
|
||||
{
|
||||
get
|
||||
{
|
||||
return Util.GetDefaultDataFileFolder();
|
||||
}
|
||||
}
|
||||
|
||||
public string DefaultLogFileFolder
|
||||
{
|
||||
get
|
||||
{
|
||||
return Util.GetDefaultLogFileFolder();
|
||||
}
|
||||
}
|
||||
|
||||
internal RestorePlan CreateRestorePlan(DatabaseRestorePlanner planner, RestoreOptions restoreOptions)
|
||||
{
|
||||
this.CreateOrUpdateRestorePlanException = null;
|
||||
RestorePlan ret = null;
|
||||
|
||||
try
|
||||
{
|
||||
ret = planner.CreateRestorePlan(restoreOptions);
|
||||
if (ret == null || ret.RestoreOperations.Count == 0)
|
||||
{
|
||||
this.ActiveException = planner.GetBackupDeviceReadErrors();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.ActiveException = ex;
|
||||
this.CreateOrUpdateRestorePlanException = this.ActiveException;
|
||||
}
|
||||
finally
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates restore plan
|
||||
/// </summary>
|
||||
public void UpdateRestorePlan(bool relocateAllFiles = false)
|
||||
{
|
||||
this.ActiveException = null; //Clear any existing exceptions as the plan is getting recreated.
|
||||
//Clear any existing exceptions as new plan is getting recreated.
|
||||
this.CreateOrUpdateRestorePlanException = null;
|
||||
this.DbFiles.Clear();
|
||||
this.planUpdateRequired = false;
|
||||
this.restorePlan = null;
|
||||
if (String.IsNullOrEmpty(this.RestorePlanner.DatabaseName))
|
||||
{
|
||||
this.RestorePlan = new RestorePlan(this.Server);
|
||||
// this.LaunchAzureConnectToStorageDialog();
|
||||
this.Util.AddCredentialNameForUrlBackupSet(this.RestorePlan, this.CredentialName);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
this.RestorePlan = this.CreateRestorePlan(this.RestorePlanner, this.RestoreOptions);
|
||||
this.Util.AddCredentialNameForUrlBackupSet(this.restorePlan, this.CredentialName);
|
||||
if (this.ActiveException == null)
|
||||
{
|
||||
this.dbFiles = this.GetDbFiles();
|
||||
if(relocateAllFiles)
|
||||
{
|
||||
RelocateDbFiles();
|
||||
}
|
||||
this.SetRestorePlanProperties(this.restorePlan);
|
||||
}
|
||||
}
|
||||
if (this.restorePlan != null)
|
||||
{
|
||||
this.RestoreSelected = new bool[this.restorePlan.RestoreOperations.Count];
|
||||
for (int i = 0; i < this.restorePlan.RestoreOperations.Count; i++)
|
||||
{
|
||||
this.RestoreSelected[i] = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RestorePlan = new RestorePlan(this.Server);
|
||||
this.Util.AddCredentialNameForUrlBackupSet(this.RestorePlan, this.CredentialName);
|
||||
this.RestoreSelected = new bool[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Determine if restore plan of selected database does have Url
|
||||
/// </summary>
|
||||
private bool IfRestorePlanHasUrl()
|
||||
{
|
||||
return (restorePlan.RestoreOperations.Any(
|
||||
res => res.BackupSet.BackupMediaSet.BackupMediaList.Any(t => t.MediaType == DeviceType.Url)));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Sets restore plan properties
|
||||
/// </summary>
|
||||
private void SetRestorePlanProperties(RestorePlan rp)
|
||||
{
|
||||
if (rp == null || rp.RestoreOperations.Count < 1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
rp.SetRestoreOptions(this.RestoreOptions);
|
||||
rp.CloseExistingConnections = this.CloseExistingConnections;
|
||||
if (this.targetDbName != null && !this.targetDbName.Equals(string.Empty))
|
||||
{
|
||||
rp.DatabaseName = targetDbName;
|
||||
}
|
||||
rp.RestoreOperations[0].RelocateFiles.Clear();
|
||||
foreach (DbFile dbFile in this.DbFiles)
|
||||
{
|
||||
// For XStore path, we don't want to try the getFullPath.
|
||||
string newPhysicalPath;
|
||||
Uri pathUri;
|
||||
bool fUriCreated = Uri.TryCreate(dbFile.PhysicalNameRelocate, UriKind.Absolute, out pathUri);
|
||||
if (fUriCreated && pathUri.Scheme == "https")
|
||||
{
|
||||
newPhysicalPath = dbFile.PhysicalNameRelocate;
|
||||
}
|
||||
else
|
||||
{
|
||||
newPhysicalPath = Path.GetFullPath(dbFile.PhysicalNameRelocate);
|
||||
}
|
||||
if (!dbFile.PhysicalName.Equals(newPhysicalPath))
|
||||
{
|
||||
RelocateFile relocFile = new RelocateFile(dbFile.LogicalName, dbFile.PhysicalNameRelocate);
|
||||
rp.RestoreOperations[0].RelocateFiles.Add(relocFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bool indicating whether a tail log backup will be taken
|
||||
/// </summary>
|
||||
public bool BackupTailLog
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.RestorePlanner.BackupTailLog;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (this.RestorePlanner.BackupTailLog != value)
|
||||
{
|
||||
this.RestorePlanner.BackupTailLog = value;
|
||||
this.planUpdateRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// bool indicating whether the database will be left in restoring state
|
||||
/// </summary>
|
||||
public bool TailLogWithNoRecovery
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.RestorePlanner.TailLogWithNoRecovery;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (this.RestorePlanner.TailLogWithNoRecovery != value)
|
||||
{
|
||||
this.RestorePlanner.TailLogWithNoRecovery = value;
|
||||
this.planUpdateRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public DateTime? CurrentRestorePointInTime
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.RestorePlan == null || this.RestorePlan.RestoreOperations.Count == 0
|
||||
|| this.RestoreSelected.Length == 0 || !this.RestoreSelected[0])
|
||||
{
|
||||
return null;
|
||||
}
|
||||
for (int i = this.RestorePlan.RestoreOperations.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (this.RestoreSelected[i])
|
||||
{
|
||||
if (this.RestorePlan.RestoreOperations[i].BackupSet == null
|
||||
|| (this.RestorePlan.RestoreOperations[i].BackupSet.BackupSetType == BackupSetType.Log &&
|
||||
this.RestorePlan.RestoreOperations[i].ToPointInTime != null))
|
||||
{
|
||||
return this.RestorePlanner.RestoreToPointInTime;
|
||||
}
|
||||
return this.RestorePlan.RestoreOperations[i].BackupSet.BackupStartDate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void ToggleSelectRestore(int index)
|
||||
{
|
||||
RestorePlan rp = this.restorePlan;
|
||||
if (rp == null || rp.RestoreOperations.Count <= index)
|
||||
{
|
||||
return;
|
||||
}
|
||||
//the last index - this will include tail-Log restore operation if present
|
||||
if (index == rp.RestoreOperations.Count - 1)
|
||||
{
|
||||
if (this.RestoreSelected[index])
|
||||
{
|
||||
this.RestoreSelected[index] = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i <= index; i++)
|
||||
{
|
||||
this.RestoreSelected[i] = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (index == 0)
|
||||
{
|
||||
if (!this.RestoreSelected[index])
|
||||
{
|
||||
this.RestoreSelected[index] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = index; i < rp.RestoreOperations.Count; i++)
|
||||
{
|
||||
this.RestoreSelected[i] = false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (index == 1 && rp.RestoreOperations[index].BackupSet.BackupSetType == BackupSetType.Differential)
|
||||
{
|
||||
if (!this.RestoreSelected[index])
|
||||
{
|
||||
this.RestoreSelected[0] = true;
|
||||
this.RestoreSelected[index] = true;
|
||||
}
|
||||
else if (rp.RestoreOperations[2].BackupSet == null)
|
||||
{
|
||||
this.RestoreSelected[index] = false;
|
||||
this.RestoreSelected[2] = false;
|
||||
}
|
||||
else if (this.Server.Version.Major < 9 || BackupSet.IsBackupSetsInSequence(rp.RestoreOperations[0].BackupSet, rp.RestoreOperations[2].BackupSet))
|
||||
{
|
||||
this.RestoreSelected[index] = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = index; i < rp.RestoreOperations.Count; i++)
|
||||
{
|
||||
this.RestoreSelected[i] = false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (rp.RestoreOperations[index].BackupSet.BackupSetType == BackupSetType.Log)
|
||||
{
|
||||
if (this.RestoreSelected[index])
|
||||
{
|
||||
for (int i = index; i < rp.RestoreOperations.Count; i++)
|
||||
{
|
||||
this.RestoreSelected[i] = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i <= index; i++)
|
||||
{
|
||||
this.RestoreSelected[i] = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the backup files location.
|
||||
/// </summary>
|
||||
internal void CheckBackupFilesLocation()
|
||||
{
|
||||
if (this.RestorePlan != null)
|
||||
{
|
||||
foreach (Restore restore in this.RestorePlan.RestoreOperations)
|
||||
{
|
||||
if (restore.BackupSet != null)
|
||||
{
|
||||
restore.BackupSet.CheckBackupFilesExistence();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal bool DbFilesLocationAreValid()
|
||||
{
|
||||
foreach (DbFile dbFile in this.DbFiles)
|
||||
{
|
||||
string newPhysicalPath = Path.GetFullPath(dbFile.PhysicalNameRelocate);
|
||||
if (string.Compare(dbFile.PhysicalName, dbFile.PhysicalNameRelocate, true) != 0)
|
||||
{
|
||||
bool isValidFolder = false;
|
||||
bool isValidPath = Util.IsDestinationPathValid(Path.GetDirectoryName(newPhysicalPath), ref isValidFolder);
|
||||
if (!(isValidFolder && isValidPath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public class RestoreDatabaseRecoveryState
|
||||
{
|
||||
public RestoreDatabaseRecoveryState(DatabaseRecoveryState recoveryState)
|
||||
{
|
||||
this.RecoveryState = recoveryState;
|
||||
}
|
||||
|
||||
public DatabaseRecoveryState RecoveryState;
|
||||
private static string RestoreWithRecovery = "RESTORE WITH RECOVERY";
|
||||
private static string RestoreWithNoRecovery = "RESTORE WITH NORECOVERY";
|
||||
private static string RestoreWithStandby = "RESTORE WITH STANDBY";
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
switch (this.RecoveryState)
|
||||
{
|
||||
case DatabaseRecoveryState.WithRecovery:
|
||||
return RestoreDatabaseRecoveryState.RestoreWithRecovery;
|
||||
case DatabaseRecoveryState.WithNoRecovery:
|
||||
return RestoreDatabaseRecoveryState.RestoreWithNoRecovery;
|
||||
case DatabaseRecoveryState.WithStandBy:
|
||||
return RestoreDatabaseRecoveryState.RestoreWithStandby;
|
||||
}
|
||||
return RestoreDatabaseRecoveryState.RestoreWithRecovery;
|
||||
}
|
||||
|
||||
/*
|
||||
public string Info()
|
||||
{
|
||||
switch (this.RecoveryState)
|
||||
{
|
||||
case DatabaseRecoveryState.WithRecovery:
|
||||
return SR.RestoreWithRecoveryInfo;
|
||||
case DatabaseRecoveryState.WithNoRecovery:
|
||||
return SR.RestoreWithNoRecoveryInfo;
|
||||
case DatabaseRecoveryState.WithStandBy:
|
||||
return SR.RestoreWithStandbyInfo;
|
||||
}
|
||||
return SR.RestoreWithRecoveryInfo;
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,740 @@
|
||||
//
|
||||
// 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.Generic;
|
||||
using Microsoft.Data.Tools.DataSets;
|
||||
using Microsoft.SqlServer.Management.Sdk.Sfc;
|
||||
using Microsoft.SqlServer.Management.Smo;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation
|
||||
{
|
||||
public class RestoreUtil
|
||||
{
|
||||
public RestoreUtil(Server server)
|
||||
{
|
||||
this.server = server;
|
||||
this.excludedDB = new List<string> { "master", "tempdb" };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current sql server instance
|
||||
/// </summary>
|
||||
private readonly Server server;
|
||||
private readonly IList<string> excludedDB;
|
||||
|
||||
public List<string> GetTargetDbNamesForPageRestore()
|
||||
{
|
||||
List<string> databaseNames = new List<string>();
|
||||
foreach (Database db in this.server.Databases)
|
||||
{
|
||||
if (!this.excludedDB.Contains(db.Name) &&
|
||||
(db.Status == DatabaseStatus.Normal || db.Status == DatabaseStatus.Suspect || db.Status == DatabaseStatus.EmergencyMode) &&
|
||||
db.RecoveryModel == RecoveryModel.Full)
|
||||
{
|
||||
databaseNames.Add(db.Name);
|
||||
}
|
||||
}
|
||||
return databaseNames;
|
||||
}
|
||||
|
||||
public List<string> GetTargetDbNames()
|
||||
{
|
||||
List<string> databaseNames = new List<string>();
|
||||
foreach (Database db in this.server.Databases)
|
||||
{
|
||||
if (!this.excludedDB.Contains(db.Name))
|
||||
{
|
||||
databaseNames.Add(db.Name);
|
||||
}
|
||||
}
|
||||
return databaseNames;
|
||||
}
|
||||
|
||||
internal DateTime GetServerCurrentDateTime()
|
||||
{
|
||||
DateTime dt = DateTime.MinValue;
|
||||
|
||||
//TODO: the code is moved from ssms and used for restore differential backups
|
||||
//Uncomment when restore operation for differential backups is supported
|
||||
/*
|
||||
string query = "SELECT GETDATE()";
|
||||
DataSet dataset = this.server.ExecutionManager.ExecuteWithResults(query);
|
||||
if (dataset != null && dataset.Tables.Count > 0 && dataset.Tables[0].Rows.Count > 0)
|
||||
{
|
||||
dt = Convert.ToDateTime(dataset.Tables[0].Rows[0][0], SmoApplication.DefaultCulture);
|
||||
}
|
||||
*/
|
||||
return dt;
|
||||
}
|
||||
|
||||
//TODO: the code is moved from ssms and used for restore differential backups
|
||||
//Uncomment when restore operation for differential backups is supported
|
||||
/*
|
||||
/// <summary>
|
||||
/// Queries msdb for source database names
|
||||
/// </summary>
|
||||
public List<String> GetSourceDbNames()
|
||||
{
|
||||
List<string> databaseNames = new List<string>();
|
||||
Request req = new Request();
|
||||
req.Urn = "Server/BackupSet";
|
||||
req.Fields = new string[1];
|
||||
req.Fields[0] = "DatabaseName";
|
||||
req.OrderByList = new OrderBy[1];
|
||||
req.OrderByList[0] = new OrderBy();
|
||||
req.OrderByList[0].Field = "DatabaseName";
|
||||
req.OrderByList[0].Dir = OrderBy.Direction.Asc;
|
||||
DataTable dt = server.ExecutionManager.GetEnumeratorData(req);
|
||||
string last = "";
|
||||
foreach (DataRow row in dt.Rows)
|
||||
{
|
||||
string dbName = Convert.ToString(row["DatabaseName"], System.Globalization.CultureInfo.InvariantCulture);
|
||||
if (!this.excludedDB.Contains(dbName) && !dbName.Equals(last))
|
||||
{
|
||||
bool found = false;
|
||||
foreach (string str in databaseNames)
|
||||
{
|
||||
if (StrEqual(str, dbName))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == false)
|
||||
{
|
||||
databaseNames.Add(dbName);
|
||||
}
|
||||
}
|
||||
last = dbName;
|
||||
}
|
||||
return databaseNames;
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Reads backup file header to get source database names
|
||||
/// If valid credential name is not provided for URL throws exception while executing T-sql statement
|
||||
/// </summary>
|
||||
/// <param name="bkdevList">List of backup device items</param>
|
||||
/// <param name="credential">Optional Sqlserver credential name to read backup header from URL</param>
|
||||
public List<String> GetSourceDbNames(ICollection<BackupDeviceItem> bkdevList, string credential = null)
|
||||
{
|
||||
List<string> databaseNames = new List<string>();
|
||||
foreach (BackupDeviceItem bkdev in bkdevList)
|
||||
{
|
||||
// use the Restore public API to do the Restore Headeronly query
|
||||
Restore res = new Restore();
|
||||
res.CredentialName = credential;
|
||||
res.Devices.Add(bkdev);
|
||||
|
||||
DataTable dt = res.ReadBackupHeader(this.server);
|
||||
if (dt != null)
|
||||
{
|
||||
foreach (DataRow dr in dt.Rows)
|
||||
{
|
||||
if (dr != null && !(dr["DatabaseName"] is DBNull))
|
||||
{
|
||||
string dbName = (string)dr["DatabaseName"];
|
||||
bool found = false;
|
||||
foreach (string str in databaseNames)
|
||||
{
|
||||
if (StringComparer.OrdinalIgnoreCase.Compare(str, dbName) == 0)
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found == false)
|
||||
{
|
||||
databaseNames.Add(dbName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return databaseNames;
|
||||
}
|
||||
|
||||
public string GetNewPhysicalRestoredFileName(string filePathParam, string dbName, bool isNewDatabase, string type, ref int fileIndex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePathParam))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
string result = string.Empty;
|
||||
string filePath = filePathParam;
|
||||
int idx = filePath.LastIndexOf('\\');
|
||||
string folderPath = filePath.Substring(0, idx);
|
||||
|
||||
string fileName = filePath.Substring(idx + 1);
|
||||
idx = fileName.LastIndexOf('.');
|
||||
string fileExtension = fileName.Substring(idx + 1);
|
||||
|
||||
bool isFolder = true;
|
||||
bool isValidPath = IsDestinationPathValid(folderPath, ref isFolder);
|
||||
|
||||
if (!isValidPath || !isFolder)
|
||||
{
|
||||
if (type != RestoreConstants.Log)
|
||||
{
|
||||
folderPath = server.Settings.DefaultFile;
|
||||
if (folderPath.Length == 0)
|
||||
{
|
||||
folderPath = server.Information.MasterDBPath;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
folderPath = server.Settings.DefaultLog;
|
||||
if (folderPath.Length == 0)
|
||||
{
|
||||
folderPath = server.Information.MasterDBLogPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!isNewDatabase)
|
||||
{
|
||||
return filePathParam;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isNewDatabase)
|
||||
{
|
||||
result = folderPath + "\\" + dbName + "." + fileExtension;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (0 != string.Compare(fileExtension, "mdf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
result = folderPath + "\\" + dbName + "_" + Convert.ToString(fileIndex, System.Globalization.CultureInfo.InvariantCulture) + "." + fileExtension;
|
||||
fileIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = folderPath + "\\" + dbName + "." + fileExtension;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool IsDestinationPathValid(string path, ref bool isFolder)
|
||||
{
|
||||
Enumerator en = null;
|
||||
DataTable dt;
|
||||
Request req = new Request();
|
||||
|
||||
en = new Enumerator();
|
||||
req.Urn = "Server/File[@FullName='" + Urn.EscapeString(path) + "']";
|
||||
dt = en.Process(this.server.ConnectionContext.SqlConnectionObject, req);
|
||||
|
||||
if (dt.Rows.Count > 0)
|
||||
{
|
||||
isFolder = !(Convert.ToBoolean(dt.Rows[0]["IsFile"], System.Globalization.CultureInfo.InvariantCulture));
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isFolder = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of database files
|
||||
/// </summary>
|
||||
/// <param name="db">SMO database</param>
|
||||
/// <returns>a list of database files</returns>
|
||||
public List<DbFile> GetDbFiles(Database db)
|
||||
{
|
||||
List<DbFile> ret = new List<DbFile>();
|
||||
if (db == null)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
char fileType = '\0';
|
||||
foreach (FileGroup fg in db.FileGroups)
|
||||
{
|
||||
if ((fg.FileGroupType == FileGroupType.FileStreamDataFileGroup) || (fg.FileGroupType == FileGroupType.MemoryOptimizedDataFileGroup))
|
||||
{
|
||||
fileType = DbFile.FileStreamFileType;
|
||||
}
|
||||
else
|
||||
{
|
||||
fileType = DbFile.RowFileType;
|
||||
}
|
||||
foreach (DataFile f in fg.Files)
|
||||
{
|
||||
DbFile dbFile = new DbFile(f.Name, fileType, f.FileName);
|
||||
ret.Add(dbFile);
|
||||
}
|
||||
}
|
||||
foreach (LogFile f in db.LogFiles)
|
||||
{
|
||||
DbFile dbFile = new DbFile(f.Name, DbFile.LogFileType, f.FileName);
|
||||
ret.Add(dbFile);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
//TODO: the code is moved from ssms and used for other typs of restore operation
|
||||
//Uncomment when restore operation for those types are supported
|
||||
/*
|
||||
public List<DbFile> GetDbFiles(BackupSet bkSet)
|
||||
{
|
||||
List<DbFile> ret = new List<DbFile>();
|
||||
if (bkSet == null || bkSet.BackupMediaSet == null || bkSet.BackupMediaSet.BackupMediaList.Count() < 1)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
DataSet dataset = bkSet.FileList;
|
||||
if (dataset != null && dataset.Tables.Count > 0)
|
||||
{
|
||||
string logicalName = null;
|
||||
string physicalName = null;
|
||||
char type = '\0';
|
||||
foreach (DataRow dr in dataset.Tables[0].Rows)
|
||||
{
|
||||
if (!(dr["LogicalName"] is DBNull))
|
||||
{
|
||||
logicalName = (string)dr["LogicalName"];
|
||||
}
|
||||
if (!(dr["PhysicalName"] is DBNull))
|
||||
{
|
||||
physicalName = (string)dr["PhysicalName"];
|
||||
}
|
||||
if (!(dr["Type"] is DBNull))
|
||||
{
|
||||
// The data type of Type in a list obtained from RESTORE FILELISTONLY is char(1).
|
||||
string temp = (string)dr["Type"];
|
||||
if (!String.IsNullOrEmpty(temp))
|
||||
{
|
||||
type = temp[0];
|
||||
}
|
||||
}
|
||||
if (!String.IsNullOrEmpty(logicalName) && !String.IsNullOrEmpty(physicalName) && (type != '\0'))
|
||||
{
|
||||
DbFile dbFile = new DbFile(logicalName, type, physicalName);
|
||||
ret.Add(dbFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of database files in all the backup devices in the Restore object
|
||||
/// </summary>
|
||||
public List<DbFile> GetDbFiles(Restore restore)
|
||||
{
|
||||
List<DbFile> ret = new List<DbFile>();
|
||||
if (restore == null || restore.Devices == null || restore.Devices.Count < 1)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
// Using the Restore public API to do the Restore FilelistOnly
|
||||
Restore res = new Restore();
|
||||
res.CredentialName = restore.CredentialName;
|
||||
res.Devices.Add(restore.Devices[0]);
|
||||
res.FileNumber = restore.FileNumber;
|
||||
DataTable datatable = res.ReadFileList(this.server);
|
||||
if (datatable != null && datatable.Rows.Count > 0)
|
||||
{
|
||||
string logicalName = null;
|
||||
string physicalName = null;
|
||||
char type = '\0';
|
||||
foreach (DataRow dr in datatable.Rows)
|
||||
{
|
||||
if (!(dr["LogicalName"] is DBNull))
|
||||
{
|
||||
logicalName = (string)dr["LogicalName"];
|
||||
}
|
||||
if (!(dr["PhysicalName"] is DBNull))
|
||||
{
|
||||
physicalName = (string)dr["PhysicalName"];
|
||||
}
|
||||
if (!(dr["Type"] is DBNull))
|
||||
{
|
||||
// The data type of Type in a list obtained from RESTORE FILELISTONLY is char(1).
|
||||
string temp = (string)dr["Type"];
|
||||
if (!String.IsNullOrEmpty(temp))
|
||||
{
|
||||
type = temp[0];
|
||||
}
|
||||
}
|
||||
if (!String.IsNullOrEmpty(logicalName) && !String.IsNullOrEmpty(physicalName) && (type != '\0'))
|
||||
{
|
||||
DbFile dbFile = new DbFile(logicalName, type, physicalName);
|
||||
ret.Add(dbFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set credential name in the restore objects which have a backup set in Microsoft Azure
|
||||
/// From sql16, default credential is SAS credential so no explict credential needed for restore object.
|
||||
/// </summary>
|
||||
/// <param name="restorePlan">Restore plan created for the restore operation</param>
|
||||
/// <param name="credentialName">Sql server credential name</param>
|
||||
public void AddCredentialNameForUrlBackupSet(RestorePlan restorePlan, string credentialName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(credentialName) || restorePlan == null || restorePlan.RestoreOperations == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (restorePlan.Server.VersionMajor >= 13) // for sql16, default backup/restore URL will use SAS
|
||||
{
|
||||
return;
|
||||
}
|
||||
// If any of the backup media in the restore object is in URL, we assign the credential name to the CredentialName property of the Restore object
|
||||
foreach (Restore res in restorePlan.RestoreOperations)
|
||||
{
|
||||
|
||||
if (res.BackupSet != null && res.BackupSet.BackupMediaSet != null && res.BackupSet.BackupMediaSet.BackupMediaList != null)
|
||||
{
|
||||
foreach (BackupMedia bkMedia in res.BackupSet.BackupMediaSet.BackupMediaList)
|
||||
{
|
||||
if (bkMedia != null && bkMedia.MediaType == DeviceType.Url)
|
||||
{
|
||||
res.CredentialName = credentialName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res.Devices != null)
|
||||
{
|
||||
foreach (BackupDeviceItem bkDevice in res.Devices)
|
||||
{
|
||||
if (bkDevice.DeviceType == DeviceType.Url)
|
||||
{
|
||||
res.CredentialName = credentialName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the backup file to which the tail log is going to be backed up is a file in Microsoft Azure,
|
||||
// we assign the credential name to the Credential Name property of the Backup object
|
||||
if (restorePlan.TailLogBackupOperation != null && restorePlan.TailLogBackupOperation.Devices != null)
|
||||
{
|
||||
foreach (BackupDeviceItem bkdevItem in restorePlan.TailLogBackupOperation.Devices)
|
||||
{
|
||||
if (bkdevItem != null && bkdevItem.DeviceType == DeviceType.Url)
|
||||
{
|
||||
restorePlan.TailLogBackupOperation.CredentialName = credentialName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal string GetDefaultDataFileFolder()
|
||||
{
|
||||
string ret = this.server.Settings.DefaultFile;
|
||||
if (string.IsNullOrEmpty(ret))
|
||||
{
|
||||
ret = this.server.Information.MasterDBPath;
|
||||
}
|
||||
|
||||
ret = ret.TrimEnd(server.PathSeparator[0]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
internal string GetDefaultLogFileFolder()
|
||||
{
|
||||
string ret = this.server.Settings.DefaultLog;
|
||||
if (string.IsNullOrEmpty(ret))
|
||||
{
|
||||
ret = this.server.Information.MasterDBLogPath;
|
||||
}
|
||||
|
||||
ret = ret.TrimEnd(server.PathSeparator[0]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
internal string GetDefaultBackupFolder()
|
||||
{
|
||||
string ret = this.server.Settings.BackupDirectory;
|
||||
ret = ret.TrimEnd(server.PathSeparator[0]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
internal string GetDefaultTailLogbackupFile(string databaseName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(databaseName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
var folderpath = GetDefaultBackupFolder();
|
||||
var filename = SanitizeFileName(databaseName) + "_LogBackup_" + GetServerCurrentDateTime().ToString("yyyy-MM-dd_HH-mm-ss") + ".bak";
|
||||
return PathWrapper.Combine(folderpath, filename);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns a default location for tail log backup
|
||||
/// If the first backup media is from Microsoft Azure, a Microsoft Azure url for the Tail log backup file is returned
|
||||
/// </summary>
|
||||
internal string GetDefaultTailLogbackupFile(string databaseName, RestorePlan restorePlan)
|
||||
{
|
||||
if (string.IsNullOrEmpty(databaseName) || restorePlan == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
if (restorePlan.TailLogBackupOperation != null && restorePlan.TailLogBackupOperation.Devices != null)
|
||||
{
|
||||
restorePlan.TailLogBackupOperation.Devices.Clear();
|
||||
}
|
||||
string folderpath = string.Empty;
|
||||
BackupMedia firstBackupMedia = this.GetFirstBackupMedia(restorePlan);
|
||||
string filename = this.SanitizeFileName(databaseName) + "_LogBackup_" + this.GetServerCurrentDateTime().ToString("yyyy-MM-dd_HH-mm-ss") + ".bak";
|
||||
if (firstBackupMedia != null && firstBackupMedia.MediaType == DeviceType.Url)
|
||||
{
|
||||
// the uri will use the same container as the container of the first backup media
|
||||
Uri uri;
|
||||
if (Uri.TryCreate(firstBackupMedia.MediaName, UriKind.Absolute, out uri))
|
||||
{
|
||||
UriBuilder uriBuilder = new UriBuilder();
|
||||
uriBuilder.Scheme = uri.Scheme;
|
||||
uriBuilder.Host = uri.Host;
|
||||
if (uri.AbsolutePath.Length > 0)
|
||||
{
|
||||
string[] parts = uri.AbsolutePath.Split('/');
|
||||
string newPath = string.Join("/", parts, 0, parts.Length - 1);
|
||||
if (newPath.EndsWith("/"))
|
||||
{
|
||||
newPath = newPath.Substring(0, newPath.Length - 1);
|
||||
}
|
||||
uriBuilder.Host = uriBuilder.Host + newPath;
|
||||
}
|
||||
uriBuilder.Path = filename;
|
||||
string urlFilename = uriBuilder.Uri.AbsoluteUri;
|
||||
if (restorePlan.TailLogBackupOperation != null && restorePlan.TailLogBackupOperation.Devices != null)
|
||||
{
|
||||
restorePlan.TailLogBackupOperation.Devices.Add(new BackupDeviceItem(urlFilename, DeviceType.Url));
|
||||
}
|
||||
return urlFilename;
|
||||
}
|
||||
}
|
||||
folderpath = this.GetDefaultBackupFolder();
|
||||
if (restorePlan.TailLogBackupOperation != null && restorePlan.TailLogBackupOperation.Devices != null)
|
||||
{
|
||||
restorePlan.TailLogBackupOperation.Devices.Add(new BackupDeviceItem(PathWrapper.Combine(folderpath, filename), DeviceType.File));
|
||||
}
|
||||
return PathWrapper.Combine(folderpath, filename);
|
||||
}
|
||||
|
||||
internal string GetDefaultStandbyFile(string databaseName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(databaseName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
var folderpath = GetDefaultBackupFolder();
|
||||
var filename = SanitizeFileName(databaseName) + "_RollbackUndo_" + GetServerCurrentDateTime().ToString("yyyy-MM-dd_HH-mm-ss") + ".bak";
|
||||
return PathWrapper.Combine(folderpath, filename);
|
||||
}
|
||||
|
||||
//TODO: the code is moved from ssms and used for other typs of restore operation
|
||||
//Uncomment when restore operation for those types are supported
|
||||
/*
|
||||
internal DateTime GetLastBackupDate(DatabaseRestorePlanner planner)
|
||||
{
|
||||
BackupSetCollection bkSetColl = planner.BackupSets;
|
||||
if (bkSetColl.backupsetList.Count > 0)
|
||||
{
|
||||
return bkSetColl.backupsetList[bkSetColl.backupsetList.Count - 1].BackupStartDate;
|
||||
}
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes the name of the file.
|
||||
/// </summary>
|
||||
/// <param name="name">The name.</param>
|
||||
/// <returns></returns>
|
||||
internal string SanitizeFileName(string name)
|
||||
{
|
||||
char[] result = name.ToCharArray();
|
||||
string illegalCharacters = "\\/:*?\"<>|";
|
||||
|
||||
int resultLength = result.GetLength(0);
|
||||
int illegalLength = illegalCharacters.Length;
|
||||
|
||||
for (int resultIndex = 0; resultIndex < resultLength; resultIndex++)
|
||||
{
|
||||
for (int illegalIndex = 0; illegalIndex < illegalLength; illegalIndex++)
|
||||
{
|
||||
if (result[resultIndex] == illegalCharacters[illegalIndex])
|
||||
{
|
||||
result[resultIndex] = '_';
|
||||
}
|
||||
}
|
||||
}
|
||||
return new string(result);
|
||||
}
|
||||
|
||||
//TODO: the code is moved from ssms and used for other typs of restore operation
|
||||
//Uncomment when restore operation for those types are supported
|
||||
/*
|
||||
internal void MarkDuplicateSuspectPages(List<SuspectPageTaskDataObject> suspectPageObjList)
|
||||
{
|
||||
List<SuspectPageTaskDataObject> newList = new List<SuspectPageTaskDataObject>(suspectPageObjList);
|
||||
newList.Sort();
|
||||
newList[0].IsDuplicate = false;
|
||||
for (int i = 1; i < newList.Count; i++)
|
||||
{
|
||||
if (newList[i].CompareTo(newList[i - 1]) == 0)
|
||||
{
|
||||
newList[i].IsDuplicate = true;
|
||||
newList[i - 1].IsDuplicate = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
newList[i].IsDuplicate = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
internal void VerifyChecksumWorker(RestorePlan plan, IBackgroundOperationContext backgroundContext, EventHandler cancelEventHandler)
|
||||
{
|
||||
if (plan == null || plan.RestoreOperations.Count() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
backgroundContext.IsCancelable = true;
|
||||
backgroundContext.CancelRequested += cancelEventHandler;
|
||||
try
|
||||
{
|
||||
foreach (Restore res in plan.RestoreOperations)
|
||||
{
|
||||
if (!backgroundContext.IsCancelRequested && res.backupSet != null)
|
||||
{
|
||||
StringBuilder bkMediaNames = new StringBuilder();
|
||||
foreach (BackupDeviceItem item in res.Devices)
|
||||
{
|
||||
backgroundContext.Status = SR.Verifying + ":" + item.Name;
|
||||
try
|
||||
{
|
||||
// Use the Restore public API to do the Restore VerifyOnly query
|
||||
Restore restore = new Restore();
|
||||
restore.CredentialName = res.CredentialName;
|
||||
restore.Devices.Add(item);
|
||||
if (!res.SqlVerify(this.server))
|
||||
{
|
||||
throw new Exception(SR.BackupDeviceItemVerificationFailed(item.Name));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception(SR.BackupDeviceItemVerificationFailed(item.Name), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
backgroundContext.CancelRequested -= cancelEventHandler;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
private BackupMedia GetFirstBackupMedia(RestorePlan restorePlan)
|
||||
{
|
||||
/*
|
||||
if (restorePlan == null || restorePlan.RestoreOperations == null || restorePlan.RestoreOperations.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Restore res = restorePlan.RestoreOperations[0];
|
||||
if (res == null || res.backupSet == null || res.backupSet.backupMediaSet == null || res.backupSet.backupMediaSet.BackupMediaList == null || res.backupSet.backupMediaSet.BackupMediaList.ToList().Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return res.backupSet.backupMediaSet.BackupMediaList.ToList()[0];
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A class representing a database file
|
||||
/// </summary>
|
||||
public class DbFile
|
||||
{
|
||||
public DbFile(string logicalName, char type, string physicalName)
|
||||
{
|
||||
this.logicalName = logicalName;
|
||||
this.physicalName = physicalName;
|
||||
if (type != '\0')
|
||||
{
|
||||
this.dbFileType = type;
|
||||
}
|
||||
this.PhysicalNameRelocate = physicalName;
|
||||
}
|
||||
|
||||
// Database file types
|
||||
// When restoring backup, the engine returns the following file type values.
|
||||
public const char RowFileType = 'D';
|
||||
public const char LogFileType = 'L';
|
||||
public const char FullTextCatalogFileType = 'F';
|
||||
public const char FileStreamFileType = 'S';
|
||||
|
||||
private string logicalName;
|
||||
public string LogicalName
|
||||
{
|
||||
get { return logicalName; }
|
||||
}
|
||||
|
||||
private string physicalName;
|
||||
public string PhysicalName
|
||||
{
|
||||
get { return physicalName; }
|
||||
}
|
||||
|
||||
internal char dbFileType;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the database file type string to be displayed in the dialog
|
||||
/// </summary>
|
||||
public string DbFileType
|
||||
{
|
||||
get
|
||||
{
|
||||
string value = string.Empty;
|
||||
switch (dbFileType)
|
||||
{
|
||||
case DbFile.RowFileType:
|
||||
value = "RowData";//TODO SR.RowData;
|
||||
break;
|
||||
case DbFile.LogFileType:
|
||||
value = "Log";// SR.Log;
|
||||
break;
|
||||
case DbFile.FileStreamFileType:
|
||||
value = "FileStream";// SR.FileStream;
|
||||
break;
|
||||
case DbFile.FullTextCatalogFileType:
|
||||
value = "FullTextCatlog";// SR.FullTextCatlog;
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
public string PhysicalNameRelocate;
|
||||
}
|
||||
}
|
||||
@@ -812,3 +812,11 @@ Backup_TaskName = Backup Database
|
||||
Task_InProgress = In progress
|
||||
Task_Completed = Completed
|
||||
|
||||
|
||||
###########################################################################
|
||||
# Restore
|
||||
ConflictWithNoRecovery = Specifying this option when restoring a backup with the NORECOVERY option is not permitted.
|
||||
InvalidPathForDatabaseFile = Invalid path for database file: '{0}'
|
||||
Log = Log
|
||||
RestorePlanFailed = Failed to create restore plan
|
||||
RestoreNotSupported = Restore database is not supported
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.4.0-preview1-25305-02" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.1" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.2" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Management.SqlScriptPublishModel" Version="140.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -32,20 +32,20 @@ namespace Microsoft.SqlTools.ServiceLayer.TaskServices
|
||||
|
||||
public event EventHandler<TaskEventArgs<TaskMessage>> MessageAdded;
|
||||
public event EventHandler<TaskEventArgs<SqlTaskStatus>> StatusChanged;
|
||||
public event EventHandler<TaskEventArgs<SqlTaskStatus>> TaskCanceled;
|
||||
|
||||
/// <summary>
|
||||
/// Creates new instance of SQL task
|
||||
/// </summary>
|
||||
/// <param name="taskMetdata">Task Metadata</param>
|
||||
/// <param name="taskToRun">The function to run to start the task</param>
|
||||
public SqlTask(TaskMetadata taskMetdata, Func<SqlTask, Task<TaskResult>> taskToRun)
|
||||
public SqlTask(TaskMetadata taskMetdata, Func<SqlTask, Task<TaskResult>> taskToRun, Func<SqlTask, Task<TaskResult>> taskToCancel)
|
||||
{
|
||||
Validate.IsNotNull(nameof(taskMetdata), taskMetdata);
|
||||
Validate.IsNotNull(nameof(taskToRun), taskToRun);
|
||||
|
||||
TaskMetadata = taskMetdata;
|
||||
TaskToRun = taskToRun;
|
||||
TaskToCancel = taskToCancel;
|
||||
StartTime = DateTime.UtcNow;
|
||||
TaskId = Guid.NewGuid();
|
||||
TokenSource = new CancellationTokenSource();
|
||||
@@ -70,6 +70,15 @@ namespace Microsoft.SqlTools.ServiceLayer.TaskServices
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The function to cancel the operation
|
||||
/// </summary>
|
||||
private Func<SqlTask, Task<TaskResult>> TaskToCancel
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Task unique id
|
||||
/// </summary>
|
||||
@@ -81,21 +90,21 @@ namespace Microsoft.SqlTools.ServiceLayer.TaskServices
|
||||
public async Task RunAsync()
|
||||
{
|
||||
TaskStatus = SqlTaskStatus.InProgress;
|
||||
await TaskToRun(this).ContinueWith(task =>
|
||||
await RunAndCancel().ContinueWith(task =>
|
||||
{
|
||||
if (task.IsCompleted && !task.IsCanceled && !task.IsFaulted)
|
||||
{
|
||||
TaskResult taskResult = task.Result;
|
||||
TaskStatus = taskResult.TaskStatus;
|
||||
}
|
||||
else if(task.IsCanceled)
|
||||
else if (task.IsCanceled)
|
||||
{
|
||||
TaskStatus = SqlTaskStatus.Canceled;
|
||||
}
|
||||
else if(task.IsFaulted)
|
||||
else if (task.IsFaulted)
|
||||
{
|
||||
TaskStatus = SqlTaskStatus.Failed;
|
||||
if(task.Exception != null)
|
||||
if (task.Exception != null)
|
||||
{
|
||||
AddMessage(task.Exception.Message);
|
||||
}
|
||||
@@ -103,6 +112,95 @@ namespace Microsoft.SqlTools.ServiceLayer.TaskServices
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a backup task for execution and cancellation
|
||||
/// </summary>
|
||||
/// <param name="sqlTask"></param>
|
||||
/// <returns></returns>
|
||||
internal async Task<TaskResult> RunAndCancel()
|
||||
{
|
||||
AddMessage(SR.Task_InProgress, SqlTaskStatus.InProgress, true);
|
||||
|
||||
TaskResult taskResult = new TaskResult();
|
||||
Task<TaskResult> performTask = TaskToRun(this);
|
||||
Task<TaskResult> completedTask = null;
|
||||
|
||||
try
|
||||
{
|
||||
if (TaskToCancel != null)
|
||||
{
|
||||
AutoResetEvent backupCompletedEvent = new AutoResetEvent(initialState: false);
|
||||
Task<TaskResult> cancelTask = Task.Run(() => CancelTaskAsync(TokenSource.Token, backupCompletedEvent));
|
||||
|
||||
completedTask = await Task.WhenAny(performTask, cancelTask);
|
||||
|
||||
// Release the cancelTask
|
||||
if (completedTask == performTask)
|
||||
{
|
||||
backupCompletedEvent.Set();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
completedTask = await Task.WhenAny(performTask);
|
||||
}
|
||||
|
||||
AddMessage(completedTask.Result.TaskStatus == SqlTaskStatus.Failed ? completedTask.Result.ErrorMessage : SR.Task_Completed,
|
||||
completedTask.Result.TaskStatus);
|
||||
taskResult = completedTask.Result;
|
||||
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
taskResult.TaskStatus = SqlTaskStatus.Canceled;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex.InnerException != null && ex.InnerException is OperationCanceledException)
|
||||
{
|
||||
taskResult.TaskStatus = SqlTaskStatus.Canceled;
|
||||
}
|
||||
else
|
||||
{
|
||||
taskResult.TaskStatus = SqlTaskStatus.Failed;
|
||||
AddMessage(ex.Message);
|
||||
}
|
||||
}
|
||||
return taskResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Async task to cancel backup
|
||||
/// </summary>
|
||||
/// <param name="backupOperation"></param>
|
||||
/// <param name="token"></param>
|
||||
/// <param name="backupCompletedEvent"></param>
|
||||
/// <returns></returns>
|
||||
private async Task<TaskResult> CancelTaskAsync(CancellationToken token, AutoResetEvent backupCompletedEvent)
|
||||
{
|
||||
// Create a task for backup cancellation request
|
||||
|
||||
TaskResult result = new TaskResult();
|
||||
WaitHandle[] waitHandles = new WaitHandle[2]
|
||||
{
|
||||
backupCompletedEvent,
|
||||
token.WaitHandle
|
||||
};
|
||||
|
||||
WaitHandle.WaitAny(waitHandles);
|
||||
try
|
||||
{
|
||||
await this.TaskToCancel(this);
|
||||
result.TaskStatus = SqlTaskStatus.Canceled;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.TaskStatus = SqlTaskStatus.Failed;
|
||||
result.ErrorMessage = ex.Message;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
//Run Task synchronously
|
||||
public void Run()
|
||||
{
|
||||
@@ -138,7 +236,10 @@ namespace Microsoft.SqlTools.ServiceLayer.TaskServices
|
||||
if (isCancelRequested != value)
|
||||
{
|
||||
isCancelRequested = value;
|
||||
OnTaskCancelRequested();
|
||||
if (isCancelRequested)
|
||||
{
|
||||
TokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -379,16 +480,6 @@ namespace Microsoft.SqlTools.ServiceLayer.TaskServices
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTaskCancelRequested()
|
||||
{
|
||||
TokenSource.Cancel();
|
||||
var handler = TaskCanceled;
|
||||
if (handler != null)
|
||||
{
|
||||
handler(this, new TaskEventArgs<SqlTaskStatus>(TaskStatus, this));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
//Dispose
|
||||
|
||||
@@ -82,12 +82,13 @@ namespace Microsoft.SqlTools.ServiceLayer.TaskServices
|
||||
/// </summary>
|
||||
/// <param name="taskMetadata">Task Metadata</param>
|
||||
/// <param name="taskToRun">The function to run the operation</param>
|
||||
/// <param name="taskToCancel">The function to cancel the operation</param>
|
||||
/// <returns></returns>
|
||||
public SqlTask CreateTask(TaskMetadata taskMetadata, Func<SqlTask, Task<TaskResult>> taskToRun)
|
||||
public SqlTask CreateTask(TaskMetadata taskMetadata, Func<SqlTask, Task<TaskResult>> taskToRun, Func<SqlTask, Task<TaskResult>> taskToCancel)
|
||||
{
|
||||
ValidateNotDisposed();
|
||||
|
||||
var newtask = new SqlTask(taskMetadata, taskToRun );
|
||||
var newtask = new SqlTask(taskMetadata, taskToRun, taskToCancel);
|
||||
|
||||
lock (lockObject)
|
||||
{
|
||||
@@ -97,6 +98,31 @@ namespace Microsoft.SqlTools.ServiceLayer.TaskServices
|
||||
return newtask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new task
|
||||
/// </summary>
|
||||
/// <param name="taskMetadata">Task Metadata</param>
|
||||
/// <param name="taskToRun">The function to run the operation</param>
|
||||
/// <returns></returns>
|
||||
public SqlTask CreateTask(TaskMetadata taskMetadata, Func<SqlTask, Task<TaskResult>> taskToRun)
|
||||
{
|
||||
return CreateTask(taskMetadata, taskToRun, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new task and starts the task
|
||||
/// </summary>
|
||||
/// <param name="taskMetadata">Task Metadata</param>
|
||||
/// <param name="taskToRun">The function to run the operation</param>
|
||||
/// <param name="taskToCancel">The function to cancel the operation</param>
|
||||
/// <returns></returns>
|
||||
public SqlTask CreateAndRun(TaskMetadata taskMetadata, Func<SqlTask, Task<TaskResult>> taskToRun, Func<SqlTask, Task<TaskResult>> taskToCancel)
|
||||
{
|
||||
var sqlTask = CreateTask(taskMetadata, taskToRun, null);
|
||||
sqlTask.Run();
|
||||
return sqlTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
|
||||
@@ -40,7 +40,7 @@ dotnet restore %REPOROOT%\test\Microsoft.SqlTools.ServiceLayer.TestDriver.Tests\
|
||||
dotnet build %REPOROOT%\test\Microsoft.SqlTools.ServiceLayer.TestDriver.Tests\Microsoft.SqlTools.ServiceLayer.TestDriver.Tests.csproj %DOTNETCONFIG%
|
||||
|
||||
SET TEST_SERVER=localhost
|
||||
SET SQLTOOLSSERVICE_EXE=%REPOROOT%\src\Microsoft.SqlTools.ServiceLayer\bin\Integration\netcoreapp2.0\win7-x64\Microsoft.SqlTools.ServiceLayer.exe
|
||||
SET SQLTOOLSSERVICE_EXE=%REPOROOT%\src\Microsoft.SqlTools.ServiceLayer\bin\Debug\netcoreapp2.0\win7-x64\MicrosoftSqlToolsServiceLayer.exe
|
||||
SET SERVICECODECOVERAGE=True
|
||||
SET CODECOVERAGETOOL="%WORKINGDIR%packages\OpenCover.4.6.684\tools\OpenCover.Console.exe"
|
||||
SET CODECOVERAGEOUTPUT=coverage.xml
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
//
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
||||
//
|
||||
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.SqlTools.Extensibility;
|
||||
using Microsoft.SqlTools.Hosting.Protocol;
|
||||
using Microsoft.SqlTools.ServiceLayer.Connection;
|
||||
using Microsoft.SqlTools.ServiceLayer.DisasterRecovery;
|
||||
using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.Contracts;
|
||||
using Microsoft.SqlTools.ServiceLayer.DisasterRecovery.RestoreOperation;
|
||||
using Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility;
|
||||
using Microsoft.SqlTools.ServiceLayer.TaskServices;
|
||||
using Microsoft.SqlTools.ServiceLayer.Test.Common;
|
||||
using Microsoft.SqlTools.ServiceLayer.UnitTests;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using static Microsoft.SqlTools.ServiceLayer.IntegrationTests.Utility.LiveConnectionHelper;
|
||||
|
||||
namespace Microsoft.SqlTools.ServiceLayer.IntegrationTests.DisasterRecovery
|
||||
{
|
||||
public class RestoreDatabaseServiceTests : ServiceTestBase
|
||||
{
|
||||
private ConnectionService _connectService = TestServiceProvider.Instance.ConnectionService;
|
||||
private Mock<IProtocolEndpoint> serviceHostMock;
|
||||
private DisasterRecoveryService service;
|
||||
|
||||
public RestoreDatabaseServiceTests()
|
||||
{
|
||||
serviceHostMock = new Mock<IProtocolEndpoint>();
|
||||
service = CreateService();
|
||||
service.InitializeService(serviceHostMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void RestorePlanShouldCreatedSuccessfullyForFullBackup()
|
||||
{
|
||||
string backupFileName = "FullBackup.bak";
|
||||
bool canRestore = true;
|
||||
await VerifyRestore(backupFileName, canRestore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void RestoreShouldExecuteSuccessfullyForFullBackup()
|
||||
{
|
||||
string backupFileName = "FullBackup.bak";
|
||||
bool canRestore = true;
|
||||
var restorePlan = await VerifyRestore(backupFileName, canRestore, true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void RestorePlanShouldFailForDiffBackup()
|
||||
{
|
||||
string backupFileName = "DiffBackup.bak";
|
||||
bool canRestore = false;
|
||||
await VerifyRestore(backupFileName, canRestore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async void RestorePlanShouldFailForTransactionLogBackup()
|
||||
{
|
||||
string backupFileName = "TransactionLogBackup.bak";
|
||||
bool canRestore = false;
|
||||
await VerifyRestore(backupFileName, canRestore);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RestorePlanRequestShouldReturnResponseWithDbFiles()
|
||||
{
|
||||
using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile())
|
||||
{
|
||||
TestConnectionResult connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath);
|
||||
|
||||
string filePath = GetBackupFilePath("FullBackup.bak");
|
||||
|
||||
RestoreParams restoreParams = new RestoreParams
|
||||
{
|
||||
BackupFilePath = filePath,
|
||||
OwnerUri = queryTempFile.FilePath
|
||||
};
|
||||
|
||||
await RunAndVerify<RestorePlanResponse>(
|
||||
test: (requestContext) => service.HandleRestorePlanRequest(restoreParams, requestContext),
|
||||
verify: ((result) =>
|
||||
{
|
||||
Assert.True(result.DbFiles.Any());
|
||||
Assert.Equal(result.DatabaseName, "BackupTestDb");
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RestoreDatabaseRequestShouldStartTheRestoreTask()
|
||||
{
|
||||
using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile())
|
||||
{
|
||||
TestConnectionResult connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath);
|
||||
|
||||
string filePath = GetBackupFilePath("FullBackup.bak");
|
||||
|
||||
RestoreParams restoreParams = new RestoreParams
|
||||
{
|
||||
BackupFilePath = filePath,
|
||||
OwnerUri = queryTempFile.FilePath
|
||||
};
|
||||
|
||||
await RunAndVerify<RestoreResponse>(
|
||||
test: (requestContext) => service.HandleRestoreRequest(restoreParams, requestContext),
|
||||
verify: ((result) =>
|
||||
{
|
||||
string taskId = result.TaskId;
|
||||
var task = SqlTaskManager.Instance.Tasks.FirstOrDefault(x => x.TaskId.ToString() == taskId);
|
||||
Assert.NotNull(task);
|
||||
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DropDatabase(string databaseName)
|
||||
{
|
||||
string dropDatabaseQuery = string.Format(CultureInfo.InvariantCulture,
|
||||
Scripts.DropDatabaseIfExist, databaseName);
|
||||
|
||||
await TestServiceProvider.Instance.RunQueryAsync(TestServerType.OnPrem, "master", dropDatabaseQuery);
|
||||
}
|
||||
|
||||
private async Task<RestorePlanResponse> VerifyRestore(string backupFileName, bool canRestore, bool execute = false)
|
||||
{
|
||||
string filePath = GetBackupFilePath(backupFileName);
|
||||
using (SelfCleaningTempFile queryTempFile = new SelfCleaningTempFile())
|
||||
{
|
||||
TestConnectionResult connectionResult = await LiveConnectionHelper.InitLiveConnectionInfoAsync("master", queryTempFile.FilePath);
|
||||
|
||||
RestoreDatabaseHelper service = new RestoreDatabaseHelper();
|
||||
var request = new RestoreParams
|
||||
{
|
||||
BackupFilePath = filePath,
|
||||
DatabaseName = string.Empty,
|
||||
OwnerUri = queryTempFile.FilePath
|
||||
};
|
||||
|
||||
var restoreDataObject = service.CreateRestoreDatabaseTaskDataObject(request);
|
||||
var response = service.CreateRestorePlanResponse(restoreDataObject);
|
||||
|
||||
Assert.NotNull(response);
|
||||
Assert.Equal(response.CanRestore, canRestore);
|
||||
if (canRestore)
|
||||
{
|
||||
Assert.True(response.DbFiles.Any());
|
||||
Assert.Equal(response.DatabaseName, "BackupTestDb");
|
||||
if(execute)
|
||||
{
|
||||
await DropDatabase(response.DatabaseName);
|
||||
Thread.Sleep(2000);
|
||||
request.RelocateDbFiles = response.RelocateFilesNeeded;
|
||||
service.ExecuteRestore(restoreDataObject);
|
||||
Assert.True(restoreDataObject.Server.Databases.Contains(response.DatabaseName));
|
||||
await DropDatabase(response.DatabaseName);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
private static string TestLocationDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
return Path.Combine(RunEnvironmentInfo.GetTestDataLocation(), "DisasterRecovery");
|
||||
}
|
||||
}
|
||||
|
||||
public DirectoryInfo BackupFileDirectory
|
||||
{
|
||||
get
|
||||
{
|
||||
string d = Path.Combine(TestLocationDirectory, "Backups");
|
||||
return new DirectoryInfo(d);
|
||||
}
|
||||
}
|
||||
|
||||
public FileInfo GetBackupFile(string fileName)
|
||||
{
|
||||
return new FileInfo(Path.Combine(BackupFileDirectory.FullName, fileName));
|
||||
}
|
||||
|
||||
private string GetBackupFilePath(string fileName)
|
||||
{
|
||||
FileInfo inputFile = GetBackupFile(fileName);
|
||||
return inputFile.FullName;
|
||||
}
|
||||
|
||||
protected DisasterRecoveryService CreateService()
|
||||
{
|
||||
CreateServiceProviderWithMinServices();
|
||||
|
||||
// Create the service using the service provider, which will initialize dependencies
|
||||
return ServiceProvider.GetService<DisasterRecoveryService>();
|
||||
}
|
||||
|
||||
protected override RegisteredServiceProvider CreateServiceProviderWithMinServices()
|
||||
{
|
||||
return CreateProvider()
|
||||
.RegisterSingleService(new DisasterRecoveryService());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,13 +24,14 @@
|
||||
<ProjectReference Include="../../src/Microsoft.SqlTools.Hosting/Microsoft.SqlTools.Hosting.csproj" />
|
||||
<ProjectReference Include="../../src/Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj" />
|
||||
<ProjectReference Include="../Microsoft.SqlTools.ServiceLayer.Test.Common/Microsoft.SqlTools.ServiceLayer.Test.Common.csproj" />
|
||||
<ProjectReference Include="..\Microsoft.SqlTools.ServiceLayer.UnitTests\Microsoft.SqlTools.ServiceLayer.UnitTests.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170427-09" />
|
||||
<PackageReference Include="xunit" Version="2.2.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.4.0-preview1-25305-02" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.1" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<PackageReference Include="xunit" Version="2.2.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.4.0-preview1-25305-02" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.1" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Scripts/CreateTestDatabaseObjects.sql" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -50,6 +50,14 @@ namespace Microsoft.SqlTools.ServiceLayer.Test.Common
|
||||
}
|
||||
}
|
||||
|
||||
public ConnectionService ConnectionService
|
||||
{
|
||||
get
|
||||
{
|
||||
return ConnectionService.Instance;
|
||||
}
|
||||
}
|
||||
|
||||
public ObjectExplorerService ObjectExplorerService
|
||||
{
|
||||
get
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<PackageReference Include="xunit" Version="2.2.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.4.0-preview1-25305-02" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.1" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.2" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Management.SqlScriptPublishModel" Version="140.2.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<PackageReference Include="xunit" Version="2.2.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
|
||||
<PackageReference Include="System.Data.SqlClient" Version="4.4.0-preview1-25305-02" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.1" />
|
||||
<PackageReference Include="Microsoft.SqlServer.Smo" Version="140.2.2" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
|
||||
@@ -31,15 +31,14 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
|
||||
|
||||
public async Task<TaskResult> FunctionToRun(SqlTask sqlTask)
|
||||
{
|
||||
sqlTask.TaskCanceled += OnTaskCanceled;
|
||||
return await Task.Factory.StartNew(() =>
|
||||
{
|
||||
while (!IsStopped)
|
||||
{
|
||||
//Just keep running
|
||||
if (cancellationTokenSource.Token.IsCancellationRequested)
|
||||
if (sqlTask.TaskStatus == SqlTaskStatus.Canceled)
|
||||
{
|
||||
throw new OperationCanceledException();
|
||||
break;
|
||||
}
|
||||
if (Failed)
|
||||
{
|
||||
@@ -53,9 +52,15 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
|
||||
});
|
||||
}
|
||||
|
||||
private void OnTaskCanceled(object sender, TaskEventArgs<SqlTaskStatus> e)
|
||||
public async Task<TaskResult> FunctionToCancel(SqlTask sqlTask)
|
||||
{
|
||||
cancellationTokenSource.Cancel();
|
||||
return await Task.Factory.StartNew(() =>
|
||||
{
|
||||
return new TaskResult
|
||||
{
|
||||
TaskStatus = SqlTaskStatus.Canceled
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,18 +16,20 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
|
||||
[Fact]
|
||||
public void CreateSqlTaskGivenInvalidArgumentShouldThrowException()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => new SqlTask(null, new DatabaseOperationStub().FunctionToRun));
|
||||
Assert.Throws<ArgumentNullException>(() => new SqlTask(new TaskMetadata(), null));
|
||||
DatabaseOperationStub operation = new DatabaseOperationStub();
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => new SqlTask(null, operation.FunctionToRun, operation.FunctionToCancel));
|
||||
Assert.Throws<ArgumentNullException>(() => new SqlTask(new TaskMetadata(), null, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateSqlTaskShouldGenerateANewId()
|
||||
{
|
||||
SqlTask sqlTask = new SqlTask(new TaskMetadata(), new DatabaseOperationStub().FunctionToRun);
|
||||
SqlTask sqlTask = new SqlTask(new TaskMetadata(), new DatabaseOperationStub().FunctionToRun, null);
|
||||
Assert.NotNull(sqlTask.TaskId);
|
||||
Assert.True(sqlTask.TaskId != Guid.Empty);
|
||||
|
||||
SqlTask sqlTask2 = new SqlTask(new TaskMetadata(), new DatabaseOperationStub().FunctionToRun);
|
||||
SqlTask sqlTask2 = new SqlTask(new TaskMetadata(), new DatabaseOperationStub().FunctionToRun, null);
|
||||
Assert.False(sqlTask.TaskId.CompareTo(sqlTask2.TaskId) == 0);
|
||||
}
|
||||
|
||||
@@ -40,7 +42,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
|
||||
{
|
||||
TaskStatus = expectedStatus
|
||||
};
|
||||
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun);
|
||||
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun, null);
|
||||
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.NotStarted);
|
||||
|
||||
Task taskToVerify = sqlTask.RunAsync().ContinueWith(task => {
|
||||
@@ -67,7 +69,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
|
||||
{
|
||||
ServerName = "server name",
|
||||
DatabaseName = "database name"
|
||||
}, operation.FunctionToRun);
|
||||
}, operation.FunctionToRun, operation.FunctionToCancel);
|
||||
|
||||
Task taskToVerify = sqlTask.RunAsync().ContinueWith(task =>
|
||||
{
|
||||
@@ -89,7 +91,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
|
||||
{
|
||||
TaskStatus = expectedStatus
|
||||
};
|
||||
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun);
|
||||
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun, operation.FunctionToCancel);
|
||||
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.NotStarted);
|
||||
|
||||
Task taskToVerify = sqlTask.RunAsync().ContinueWith(task => {
|
||||
@@ -111,7 +113,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
|
||||
operation.TaskResult = new TaskResult
|
||||
{
|
||||
};
|
||||
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun);
|
||||
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun, operation.FunctionToCancel);
|
||||
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.NotStarted);
|
||||
|
||||
Task taskToVerify = sqlTask.RunAsync().ContinueWith(task => {
|
||||
@@ -133,7 +135,7 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
|
||||
operation.TaskResult = new TaskResult
|
||||
{
|
||||
};
|
||||
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun);
|
||||
SqlTask sqlTask = new SqlTask(new TaskMetadata(), operation.FunctionToRun, operation.FunctionToCancel);
|
||||
Assert.Equal(sqlTask.TaskStatus, SqlTaskStatus.NotStarted);
|
||||
|
||||
Task taskToVerify = sqlTask.RunAsync().ContinueWith(task => {
|
||||
|
||||
@@ -71,12 +71,12 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
|
||||
operation.TaskResult = new TaskResult
|
||||
{
|
||||
};
|
||||
SqlTask sqlTask = manager.CreateTask(taskMetaData, operation.FunctionToRun);
|
||||
SqlTask sqlTask = manager.CreateTask(taskMetaData, operation.FunctionToRun, operation.FunctionToCancel);
|
||||
Assert.NotNull(sqlTask);
|
||||
|
||||
Task taskToVerify = sqlTask.RunAsync().ContinueWith(task =>
|
||||
{
|
||||
Assert.Equal(sqlTask.TaskStatus, expectedStatus);
|
||||
Assert.Equal(expectedStatus, sqlTask.TaskStatus);
|
||||
Assert.Equal(sqlTask.IsCancelRequested, true);
|
||||
manager.Reset();
|
||||
|
||||
|
||||
@@ -69,11 +69,11 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.TaskServices
|
||||
serviceHostMock.AddEventHandling(TaskCreatedNotification.Type, null);
|
||||
serviceHostMock.AddEventHandling(TaskStatusChangedNotification.Type, null);
|
||||
DatabaseOperationStub operation = new DatabaseOperationStub();
|
||||
SqlTask sqlTask = service.TaskManager.CreateTask(taskMetaData, operation.FunctionToRun);
|
||||
SqlTask sqlTask = service.TaskManager.CreateTask(taskMetaData, operation.FunctionToRun, operation.FunctionToCancel);
|
||||
Task taskToVerify = sqlTask.RunAsync().ContinueWith(task =>
|
||||
{
|
||||
serviceHostMock.Verify(x => x.SendEvent(TaskStatusChangedNotification.Type,
|
||||
It.Is<TaskProgressInfo>(t => t.Status == SqlTaskStatus.Canceled)), Times.Once());
|
||||
It.Is<TaskProgressInfo>(t => t.Status == SqlTaskStatus.Canceled)), Times.AtLeastOnce());
|
||||
});
|
||||
CancelTaskParams cancelParams = new CancelTaskParams
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user