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 /// /// used for output from processes /// private static StringBuilder sb = new StringBuilder(); private static StringBuilder sbError = new StringBuilder(); /// /// lock to prevent multiple threads operating on sb simultaneously /// private static readonly object PROCESS_LOCK = new object(); #endregion properties #region methods /// /// used to collect output from a running process /// /// /// 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); } } /// /// log function optionally passed into functions /// /// public delegate void LogDelegate(params object[] paramlist); /// /// starts a process with a given command and logs /// /// /// /// /// 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; } /// /// handles any error output from running a SqlCmd.exe process /// 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); } } /// /// gets the path to SqlServer /// /// 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"); } /// /// 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" /// /// 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; } /// /// installed, and run the command passed in. /// /// Get the path to the latest version of SQL Server Express LocalDB /// /// 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; } /// /// attaches to a given database /// throws DbNotAttached exception /// /// /// /// /// /// /// 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); } } /// /// checks to see if database files exist /// throws FileNotFound exception /// /// /// 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); } } /// /// creates an SQL local db instance /// throws FailedToCreateInstance exception /// /// /// public static void CreateInstance(string instance, LogDelegate log) { var resultString = ProcessSqlLocalDbCommand($"create {instance}", log); if (resultString.Length != 0) { throw new SqlServerLocalDbException(SqlServerLocalDbException.Errors.FailedToCreateInstance); } } /// /// stops a given sql local db instance /// throws LocalDbDoesntExist exception /// /// /// 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); } } /// /// deletes a given sql local db instance /// throws FailedToDeleteInstance exception /// /// /// public static void DeleteInstance(string instance, LogDelegate log) { var resultString = ProcessSqlLocalDbCommand($"delete {instance}", log); if (resultString.Length != 0) { throw new SqlServerLocalDbException(SqlServerLocalDbException.Errors.FailedToDeleteInstance); } } /// /// starts a local db instance /// throws FailedToStartInstance exception /// /// /// 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, } } } }