495 lines
23 KiB
C#
495 lines
23 KiB
C#
using Microsoft.Win32;
|
|
using System;
|
|
using System.Diagnostics;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Text;
|
|
|
|
namespace DTS.Common.Utils
|
|
{
|
|
public static class Database
|
|
{
|
|
#region properties
|
|
/// <summary>
|
|
/// used for output from processes
|
|
/// </summary>
|
|
private static StringBuilder sb = new StringBuilder();
|
|
private static StringBuilder sbError = new StringBuilder();
|
|
/// <summary>
|
|
/// lock to prevent multiple threads operating on sb simultaneously
|
|
/// </summary>
|
|
private static readonly object PROCESS_LOCK = new object();
|
|
#endregion properties
|
|
#region methods
|
|
/// <summary>
|
|
/// used to collect output from a running process
|
|
/// </summary>
|
|
/// <param name="sendingProcess"></param>
|
|
/// <param name="outLine"></param>
|
|
private static void OutputHandler(object sendingProcess, DataReceivedEventArgs outLine)
|
|
{
|
|
if (outLine.Data != null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(outLine.Data))
|
|
{
|
|
sb.Append("\r\n");
|
|
}
|
|
|
|
sb.Append(outLine.Data);
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// log function optionally passed into functions
|
|
/// </summary>
|
|
/// <param name="paramlist"></param>
|
|
public delegate void LogDelegate(params object[] paramlist);
|
|
|
|
/// <summary>
|
|
/// starts a process with a given command and logs
|
|
/// </summary>
|
|
/// <param name="sqlLocalDbExeFileName"></param>
|
|
/// <param name="command"></param>
|
|
/// <param name="log"></param>
|
|
/// <returns></returns>
|
|
private static string SqlCommandProcessor(string sqlLocalDbExeFileName, string command, LogDelegate log)
|
|
{
|
|
var resultString = string.Empty;
|
|
lock (PROCESS_LOCK)
|
|
{
|
|
sb.Clear();
|
|
sbError.Clear();
|
|
var process = new Process
|
|
{
|
|
StartInfo =
|
|
{
|
|
FileName = sqlLocalDbExeFileName,
|
|
Arguments = command,
|
|
LoadUserProfile = true,
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
CreateNoWindow = true
|
|
}
|
|
};
|
|
//* Set ONLY ONE handler here.
|
|
process.OutputDataReceived += OutputHandler;
|
|
process.ErrorDataReceived += Process_ErrorDataReceived;
|
|
//* Start process
|
|
process.Start();
|
|
//* Read one element asynchronously
|
|
process.BeginErrorReadLine();
|
|
//* Read the other one synchronously
|
|
var output = process.StandardOutput.ReadToEnd();
|
|
Console.WriteLine(output);
|
|
log?.Invoke($"Result of {command} command is: {output}");
|
|
|
|
process.WaitForExit();
|
|
if (sb.Length > 0)
|
|
{
|
|
resultString = sb.ToString();
|
|
}
|
|
if (sbError.Length > 0)
|
|
{
|
|
log?.Invoke($"Error command: {command} error: {sbError}");
|
|
resultString += sbError.ToString();
|
|
}
|
|
}
|
|
|
|
return resultString;
|
|
}
|
|
/// <summary>
|
|
/// handles any error output from running a SqlCmd.exe process
|
|
/// </summary>
|
|
private static void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
|
|
{
|
|
if (e.Data != null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(e.Data))
|
|
{
|
|
sbError.Append("\r\n");
|
|
}
|
|
sbError.Append(e.Data);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// gets the path to SqlServer
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public static string GetSqlServerLocalDbPath()
|
|
{
|
|
var highestVersionInstalledPath = string.Empty;
|
|
|
|
var rk = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
|
|
var sk1 = rk.OpenSubKey("SOFTWARE\\Microsoft\\Microsoft SQL Server Local DB\\Installed Versions");
|
|
if (sk1 == null) return string.Empty;
|
|
var maxProductVersion = 0.0;
|
|
foreach (var productSubKeyName in sk1.GetSubKeyNames())
|
|
{
|
|
if (!double.TryParse(productSubKeyName, NumberStyles.Float, CultureInfo.InvariantCulture, out var thisVersion)) continue;
|
|
if (thisVersion < maxProductVersion) continue;
|
|
maxProductVersion = thisVersion;
|
|
var newKey = sk1.OpenSubKey(productSubKeyName);
|
|
if (newKey == null) continue;
|
|
var val = newKey.GetValue("InstanceAPIPath", -1, RegistryValueOptions.None).ToString();
|
|
if (val == "-1" || !val.EndsWith("SqlUserInstance.dll")) continue;
|
|
highestVersionInstalledPath = val.Substring(0, val.Length - "SqlUserInstance.dll".Length);
|
|
}
|
|
|
|
var envProgFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
|
Utilities.Logging.APILogger.Log($"Environment.Program Files is {envProgFiles}");
|
|
var envProgFilesNoDrive = envProgFiles.Substring(envProgFiles.LastIndexOf("\\") + 1);
|
|
Utilities.Logging.APILogger.Log($"envProgFilesNoDrive is {envProgFilesNoDrive}");
|
|
Utilities.Logging.APILogger.Log($"highestVersionInstalledPath before Replace is {highestVersionInstalledPath}");
|
|
highestVersionInstalledPath = highestVersionInstalledPath.Replace("Program Files", envProgFilesNoDrive);
|
|
Utilities.Logging.APILogger.Log($"highestVersionInstalledPath after Replace is {highestVersionInstalledPath}");
|
|
|
|
return highestVersionInstalledPath.Replace("LocalDB", "Tools");
|
|
}
|
|
/// <summary>
|
|
/// Instead of relying on the Path environment variable to have the path to the SQLCMD.EXE that
|
|
/// we need to run, earlier in the Path list than a path to an older version of SQLCMD.EXE that
|
|
/// will not work with the SqlLocalDb version that we are using, search the registry for the
|
|
/// newest version of SQLCMD.exe and return that path.
|
|
/// The path should be something like $"C:\\Program Files\\Microsoft SQL Server\\Client SDK\\ODBC\\110\\Tools\\Binn"
|
|
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public static string GetODBCToolsPath(LogDelegate log)
|
|
{
|
|
var ODBCToolsPath = string.Empty;
|
|
|
|
try
|
|
{
|
|
var rk = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
|
|
var sk1 = rk.OpenSubKey("SOFTWARE\\Microsoft\\Microsoft SQL Server");
|
|
if (sk1 == null) return string.Empty;
|
|
ODBCToolsPath = ScanRegistry(log, sk1, true);
|
|
if (string.IsNullOrWhiteSpace(ODBCToolsPath))
|
|
{
|
|
//We didn't find the patch using the previous method (ensuring that it had a CurrentVersion sub folder)
|
|
//so try to find the path without that sub folder.
|
|
ODBCToolsPath = ScanRegistry(log, sk1, false);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log?.Invoke(ex.Message);
|
|
}
|
|
|
|
finally
|
|
{
|
|
log?.Invoke($"ODBCToolsPath is {ODBCToolsPath}");
|
|
}
|
|
|
|
return ODBCToolsPath;
|
|
// This should return something like $"C:\\Program Files\\Microsoft SQL Server\\Client SDK\\ODBC\\110\\Tools\\Binn";
|
|
}
|
|
private static string ScanRegistry(LogDelegate log, RegistryKey sk1, bool checkCurrentVersion)
|
|
{
|
|
var ODBCToolsPath = string.Empty;
|
|
|
|
var envProgFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
|
Utilities.Logging.APILogger.Log($"Environment.Program Files is {envProgFiles}");
|
|
var envProgFilesNoDrive = envProgFiles.Substring(envProgFiles.LastIndexOf("\\") + 1);
|
|
Utilities.Logging.APILogger.Log($"envProgFilesNoDrive is {envProgFilesNoDrive}");
|
|
|
|
try
|
|
{
|
|
var maxProductVersion = 0;
|
|
var maxProductSubKeyNameInt = 0;
|
|
foreach (var productSubKeyName in sk1.GetSubKeyNames())
|
|
{
|
|
if (!double.TryParse(productSubKeyName, NumberStyles.Float, CultureInfo.InvariantCulture, out var thisVersion)) continue;
|
|
var productSubKey = sk1.OpenSubKey(productSubKeyName);
|
|
if (productSubKey == null) continue;
|
|
foreach (var folderName in productSubKey.GetSubKeyNames())
|
|
{
|
|
if (folderName == "Tools")
|
|
{
|
|
//e.g. Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\120\Tools
|
|
var toolsFolderSubkey = productSubKey.OpenSubKey(folderName);
|
|
if (toolsFolderSubkey == null) continue;
|
|
foreach (var toolsSubFolderName in toolsFolderSubkey.GetSubKeyNames())
|
|
{
|
|
if (toolsSubFolderName == "ClientSetup")
|
|
{
|
|
//e.g. Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\120\Tools\ClientSetup
|
|
var clientSetupFolderSubkey = toolsFolderSubkey.OpenSubKey(toolsSubFolderName);
|
|
if (clientSetupFolderSubkey == null) continue;
|
|
var pathValue = clientSetupFolderSubkey.GetValue("ODBCToolsPath", -1, RegistryValueOptions.None).ToString();
|
|
if (pathValue == "-1")
|
|
{
|
|
log?.Invoke($"There is no ODBCToolsPath subkey in {clientSetupFolderSubkey}");
|
|
continue;
|
|
}
|
|
Utilities.Logging.APILogger.Log($"pathValue before Replace is {pathValue}");
|
|
pathValue = pathValue.Replace("Program Files", envProgFilesNoDrive);
|
|
Utilities.Logging.APILogger.Log($"pathValue after Replace is {pathValue}");
|
|
|
|
var sqlcmdFile = Path.Combine(pathValue, "SQLCMD.EXE");
|
|
log?.Invoke($"Looking for {sqlcmdFile}");
|
|
if (!File.Exists(sqlcmdFile))
|
|
{
|
|
log?.Invoke($"No file named {sqlcmdFile} exists");
|
|
//Try without the "\Client SDK\ODBC\" folders
|
|
pathValue = pathValue.Replace("Client SDK\\ODBC\\", "");
|
|
sqlcmdFile = Path.Combine(pathValue, "SQLCMD.EXE");
|
|
log?.Invoke($"Now looking for {sqlcmdFile}");
|
|
if (!File.Exists(sqlcmdFile)) continue;
|
|
log?.Invoke($"Found {sqlcmdFile}");
|
|
}
|
|
if (checkCurrentVersion)
|
|
{
|
|
foreach (var clientSetupSubFolderName in clientSetupFolderSubkey.GetSubKeyNames())
|
|
{
|
|
if (clientSetupSubFolderName == "CurrentVersion")
|
|
{
|
|
//Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\120\Tools\ClientSetup\CurrentVersion
|
|
var currentVersionFolderSubkey = clientSetupFolderSubkey.OpenSubKey(clientSetupSubFolderName);
|
|
if (currentVersionFolderSubkey == null) continue;
|
|
var versionValue = currentVersionFolderSubkey.GetValue("CurrentVersion", -1, RegistryValueOptions.None).ToString();
|
|
if (versionValue == "-1") continue;
|
|
var majorVersionString = versionValue.Split('.')[0];
|
|
if (!Int32.TryParse(majorVersionString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var majorVersionInt)) continue;
|
|
if (majorVersionInt > maxProductVersion)
|
|
{
|
|
maxProductVersion = majorVersionInt;
|
|
ODBCToolsPath = pathValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!Int32.TryParse(productSubKeyName, NumberStyles.Integer, CultureInfo.InvariantCulture, out var productSubKeyNameInt)) continue;
|
|
if (productSubKeyNameInt > maxProductSubKeyNameInt)
|
|
{
|
|
maxProductSubKeyNameInt = productSubKeyNameInt;
|
|
ODBCToolsPath = pathValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log?.Invoke(ex.Message);
|
|
}
|
|
finally
|
|
{
|
|
log?.Invoke($"ODBCToolsPath is {ODBCToolsPath}");
|
|
}
|
|
|
|
return ODBCToolsPath;
|
|
}
|
|
/// <summary>
|
|
/// installed, and run the command passed in.
|
|
/// </summary>
|
|
/// Get the path to the latest version of SQL Server Express LocalDB
|
|
/// <param name="command"></param>
|
|
/// <returns></returns>
|
|
private static string ProcessSqlLocalDbCommand(string command, LogDelegate log)
|
|
{
|
|
//SQL Server Express LocalDB 2014 is a Prerequisite of the DataPRO Installer,
|
|
//so it should be there unless it has been subsequently uninstalled.
|
|
var localDbPath = GetSqlServerLocalDbPath();
|
|
if (localDbPath == string.Empty)
|
|
{
|
|
//SQL Server LocalDb is not installed so display error and go away
|
|
throw new SqlServerLocalDbException(SqlServerLocalDbException.Errors.LocalDbDoesntExist);
|
|
}
|
|
var sqlLocalDbExeFileName = localDbPath + "SqlLocalDB.exe";
|
|
return SqlCommandProcessor(sqlLocalDbExeFileName, command, log);
|
|
}
|
|
private static string BatchCommandProcessor(string batchFileName,
|
|
string dbName,
|
|
string sqlDbFileName,
|
|
string sqlLogFileName,
|
|
string fullSqlcmdPath,
|
|
LogDelegate log)
|
|
{
|
|
var resultString = string.Empty;
|
|
lock (PROCESS_LOCK)
|
|
{
|
|
sb.Clear();
|
|
sbError.Clear();
|
|
var process = new Process
|
|
{
|
|
StartInfo =
|
|
{
|
|
FileName = batchFileName,
|
|
Arguments = dbName + " " + "\"" + sqlDbFileName + "\"" + " " + "\"" + sqlLogFileName + "\"" + " " + "\"" + fullSqlcmdPath + "\"",
|
|
LoadUserProfile = true,
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
CreateNoWindow = true
|
|
}
|
|
};
|
|
//* Set ONLY ONE handler here.
|
|
process.OutputDataReceived += OutputHandler;
|
|
process.ErrorDataReceived += Process_ErrorDataReceived;
|
|
//* Start process
|
|
process.Start();
|
|
//* Read one element asynchronously
|
|
process.BeginErrorReadLine();
|
|
//* Read the other one synchronously
|
|
var output = process.StandardOutput.ReadToEnd();
|
|
Console.WriteLine(output);
|
|
log?.Invoke($"Result of attach {dbName} using {sqlDbFileName} and {sqlLogFileName} is:");
|
|
log?.Invoke(output);
|
|
process.WaitForExit();
|
|
if (sb.Length > 0)
|
|
{
|
|
resultString = sb.ToString();
|
|
}
|
|
if (sbError.Length > 0)
|
|
{
|
|
resultString += sbError.ToString();
|
|
}
|
|
}
|
|
|
|
return resultString;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// attaches to a given database
|
|
/// throws DbNotAttached exception
|
|
/// </summary>
|
|
/// <param name="targetDir"></param>
|
|
/// <param name="dbName"></param>
|
|
/// <param name="dbFolder"></param>
|
|
/// <param name="scriptsFolder"></param>
|
|
/// <param name="attachDBsbat"></param>
|
|
/// <param name="log"></param>
|
|
public static void AttachDatabase(string targetDir,
|
|
string dbName,
|
|
string dbFolder,
|
|
string scriptsFolder,//StringResources.ScriptsFolder
|
|
string attachDBsbat,
|
|
LogDelegate log) //StringResources.AttachDBsbat
|
|
{
|
|
const string SqlCmdExe = "sqlcmd.exe";
|
|
|
|
var dbFileName = Path.Combine(Environment.CurrentDirectory, dbFolder, dbName) + ".mdf";
|
|
var logFileName = Path.Combine(Environment.CurrentDirectory, dbFolder, dbName) + "_log.ldf";
|
|
var batchFileName = Path.Combine(targetDir, scriptsFolder, attachDBsbat);
|
|
var oDBCToolsPath = DTS.Common.Utils.Database.GetODBCToolsPath(log);
|
|
var fullSqlcmdPath = Path.Combine(oDBCToolsPath, SqlCmdExe); //e.g. $"\"C:\\Program Files\\Microsoft SQL Server\\Client SDK\\ODBC\\110\\Tools\\Binn\\sqlcmd.exe\""
|
|
var resultString = BatchCommandProcessor(batchFileName, dbName, dbFileName, logFileName, fullSqlcmdPath, log);
|
|
if (resultString.Length != 0)
|
|
{
|
|
throw new SqlServerLocalDbException(SqlServerLocalDbException.Errors.DbNotAttached);
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// checks to see if database files exist
|
|
/// throws FileNotFound exception
|
|
/// </summary>
|
|
/// <param name="defaultDbName"></param>
|
|
/// <param name="dbFolder"></param>
|
|
public static void CheckLocalDatabaseFilesExist(string defaultDbName, string dbFolder)
|
|
{
|
|
var dbFileNameSource = Path.Combine(Environment.CurrentDirectory, dbFolder, defaultDbName) + ".mdf";
|
|
var logFileNameSource = Path.Combine(Environment.CurrentDirectory, dbFolder, defaultDbName) + "_log.ldf";
|
|
if (!File.Exists(dbFileNameSource))
|
|
{
|
|
throw new FileNotFoundException(dbFileNameSource);
|
|
}
|
|
if (!File.Exists(logFileNameSource))
|
|
{
|
|
throw new FileNotFoundException(logFileNameSource);
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// creates an SQL local db instance
|
|
/// throws FailedToCreateInstance exception
|
|
/// </summary>
|
|
/// <param name="instance"></param>
|
|
/// <param name="log"></param>
|
|
public static void CreateInstance(string instance, LogDelegate log)
|
|
{
|
|
var resultString = ProcessSqlLocalDbCommand($"create {instance}", log);
|
|
if (resultString.Length != 0)
|
|
{
|
|
throw new SqlServerLocalDbException(SqlServerLocalDbException.Errors.FailedToCreateInstance);
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// stops a given sql local db instance
|
|
/// throws LocalDbDoesntExist exception
|
|
/// </summary>
|
|
/// <param name="instance"></param>
|
|
/// <param name="log"></param>
|
|
public static void StopInstance(string instance, LogDelegate log)
|
|
{
|
|
var resultString = ProcessSqlLocalDbCommand($"stop {instance}", log);
|
|
if (resultString.Length != 0)
|
|
{
|
|
throw new SqlServerLocalDbException(SqlServerLocalDbException.Errors.FailedToStopInstance, resultString);
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// deletes a given sql local db instance
|
|
/// throws FailedToDeleteInstance exception
|
|
/// </summary>
|
|
/// <param name="instance"></param>
|
|
/// <param name="log"></param>
|
|
public static void DeleteInstance(string instance, LogDelegate log)
|
|
{
|
|
var resultString = ProcessSqlLocalDbCommand($"delete {instance}", log);
|
|
if (resultString.Length != 0)
|
|
{
|
|
throw new SqlServerLocalDbException(SqlServerLocalDbException.Errors.FailedToDeleteInstance);
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// starts a local db instance
|
|
/// throws FailedToStartInstance exception
|
|
/// </summary>
|
|
/// <param name="instance"></param>
|
|
/// <param name="log"></param>
|
|
public static void StartInstance(string instance, LogDelegate log)
|
|
{
|
|
var resultString = ProcessSqlLocalDbCommand($"start {instance}", log);
|
|
if (resultString.Length != 0)
|
|
{
|
|
throw new SqlServerLocalDbException(SqlServerLocalDbException.Errors.FailedToStartInstance);
|
|
}
|
|
}
|
|
#endregion methods
|
|
public class SqlServerLocalDbException
|
|
: Exception
|
|
{
|
|
public Errors Error { get; }
|
|
|
|
public SqlServerLocalDbException(Errors error)
|
|
{
|
|
Error = error;
|
|
}
|
|
public SqlServerLocalDbException(Errors error, string msg)
|
|
: base(msg)
|
|
{
|
|
Error = error;
|
|
}
|
|
public enum Errors
|
|
{
|
|
FailedToStopInstance,
|
|
LocalDbDoesntExist,//sql local db doesn't exist
|
|
FailedToDeleteInstance,
|
|
FailedToCreateInstance,
|
|
FailedToStartInstance,
|
|
DbNotAttached,
|
|
IsodbNotAttached,
|
|
DASFactoryDBNotAttached,
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|