This commit is contained in:
2026-04-17 14:55:32 -04:00
commit bc3ac1d4c9
18017 changed files with 4371742 additions and 0 deletions

View File

@@ -0,0 +1,71 @@
namespace DbAPI.Connections
{
/// <summary>
/// Basic implementation of <see cref="IConnectionDetails"/>
/// <inheritdoc cref="IConnectionDetails" />
/// </summary>
public class ConnectionDetails : IConnectionDetails
{
public virtual bool UseNTLMAuthentication { get; set; } = true;
public virtual string DbUser { get; set; } = "DataPROUser";
public virtual string DbUserPassword { get; set; } = "DTSSealBeachHQ";
public virtual string InstanceName { get; set; } = "DataPROInstance";
public virtual string DbServer { get; set; } = @"(localdb)\DataPROInstance";
public virtual string DbName { get; set; } = "DataPRO";
public virtual bool UsingCentralizedDb { get; set; } = false;
public virtual string DbFolderPath { get; set; } = "";
public virtual string AttachDbsBatPath { get; set; } = "";
public virtual string SqlDbPath { get; set; } = "";
public virtual string ODBCToolsPath { get; set; } = "";
/// <summary>
/// holds the code version of the database, what we would use if available
/// should be initialized by clients
/// </summary>
public int ClientDbVersion { get; set; }
/// <summary>
/// holds the version of the database, what we will be using
/// should be initialized by clients
/// </summary>
public int ConnectionDbVersion { get; set; }
protected string _Connection = null;
public virtual string GetConnectionString()
{
if (null != _Connection) { return _Connection; }
_Connection = UseNTLMAuthentication
? $"Server={DbServer};Database={DbName};Trusted_Connection=TRUE;"
: $"Server={DbServer};Database={DbName};User Id={DbUser};Password={DbUserPassword};";
return _Connection;
}
public override string ToString()
{
return UsingCentralizedDb ? $"{DbServer}\\{DbName}" : $"Local\\{DbName}";
}
public ConnectionDetails() { }
public ConnectionDetails(ConnectionDetails details)
{
UseNTLMAuthentication = details.UseNTLMAuthentication;
DbUser = details.DbUser;
DbUserPassword = details.DbUserPassword;
InstanceName = details.InstanceName;
DbServer = details.DbServer;
DbName = details.DbName;
UsingCentralizedDb = details.UsingCentralizedDb;
DbFolderPath = details.DbFolderPath;
AttachDbsBatPath = details.AttachDbsBatPath;
SqlDbPath = details.SqlDbPath;
ODBCToolsPath = details.ODBCToolsPath;
_Connection = details._Connection;
ClientDbVersion = details.ClientDbVersion;
ConnectionDbVersion = details.ConnectionDbVersion;
}
public virtual IConnectionDetails Clone()
{
return new ConnectionDetails(this);
}
}
}

View File

@@ -0,0 +1,475 @@
using DbAPI.Errors;
using DbAPI.Logging;
using DTS.Common.Interface.Database;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace DbAPI.Connections
{
/// <summary>
/// internal class, no real reason to export this class, it handles all the details for the interface we exported
/// Implements <see cref="IConnections"/>
/// <inheritdoc cref="IConnections"/>
/// </summary>
internal class ConnectionManager : IConnections
{
private static readonly object LOGIN_LOCK = new object();
private List<Tuple<IUserDbRecord, IConnectionDetails>> _loggedInUsers = new List<Tuple<IUserDbRecord, IConnectionDetails>>();
private static readonly object CONNECT_LOCK = new object();
private List<IConnectionDetails> _connections = new List<IConnectionDetails>();
private void AddLoggedInUser(IConnectionDetails con, IUserDbRecord user)
{
lock (LOGIN_LOCK)
{
_loggedInUsers.Add(new Tuple<IUserDbRecord, IConnectionDetails>(user, con));
}
}
/// <summary>
/// removes all logged in users and all database connections
/// </summary>
public void ClearConnections()
{
lock (LOGIN_LOCK)
{
_loggedInUsers.Clear();
}
lock (CONNECT_LOCK)
{
_connections.Clear();
}
}
public ulong ConnectToDb(IConnectionDetails details)
{
try
{
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"ConnectToDb {details}");
var ret = Connect(details);
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"Connection result {ErrorCodes.ResultToString(ret)}");
return ret;
}
catch (Exception ex)
{
Console.WriteLine($"Exception: {ex.Message}");
LogManager.Log(TraceEventType.Error, LogManager.LogEvents.Connections, ex.Message);
return ErrorCodes.ERROR_UNKNOWN;
}
}
public IConnectionDetails[] GetActiveConnections()
{
lock (CONNECT_LOCK)
{
return _connections.ToArray();
}
}
public ulong LoginUserHash(IConnectionDetails connection, string user, string hash, out IUserDbRecord userObject)
{
userObject = null;
try
{
var res = User.User.GetUser(connection, out var foundUser, user);
if (res != ErrorCodes.ERROR_SUCCESS)
{
return ErrorCodes.ERROR_LOGINFAILED;
}
if (hash != foundUser.Password)
{
return ErrorCodes.ERROR_LOGINFAILED;
}
userObject = foundUser;
AddLoggedInUser(connection, userObject);
return ErrorCodes.ERROR_SUCCESS;
}
catch (Exception ex)
{
LogManager.Log(TraceEventType.Error, LogManager.LogEvents.Login, $"Login {connection} Error: {ex.Message}");
}
return ErrorCodes.ERROR_UNKNOWN;
}
public ulong LoginUser(IConnectionDetails connection, string user, string password, out IUserDbRecord userObject)
{
userObject = null;
try
{
if (null == connection)
{
return ErrorCodes.ERROR_MISSING_PARAMETER;
}
if (string.IsNullOrEmpty(user))
{
return ErrorCodes.ERROR_MISSING_PARAMETER;
}
var res = User.User.GetUser(connection, out var foundUser, user);
if (res != ErrorCodes.ERROR_SUCCESS)
{
return ErrorCodes.ERROR_LOGINFAILED;
}
var b = Encoding.UTF8.GetBytes(string.Format("{0}_{1}", password, user));
var sha2 = SHA256.Create();
var hashed = sha2.ComputeHash(b);
var hashedEncoded = Convert.ToBase64String(hashed);
if (hashedEncoded != foundUser.Password)
{
return ErrorCodes.ERROR_LOGINFAILED;
}
userObject = foundUser;
AddLoggedInUser(connection, userObject);
return ErrorCodes.ERROR_SUCCESS;
}
catch (Exception ex)
{
LogManager.Log(TraceEventType.Error, LogManager.LogEvents.Login, $"Login {connection} Error: {ex.Message}");
}
return ErrorCodes.ERROR_UNKNOWN;
}
public Tuple<IUserDbRecord, IConnectionDetails>[] GetLoggedInUsers()
{
lock (LOGIN_LOCK)
{
return _loggedInUsers.ToArray();
}
}
private ulong Connect(IConnectionDetails details)
{
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"Connecting: {details}");
var copy = details.Clone();
ulong result;
if (copy.UsingCentralizedDb)
{
result = ConnectRemote(details);
}
else
{
result = ConnectLocal(copy);
}
if (result == Errors.ErrorCodes.ERROR_SUCCESS)
{
AddConnection(copy);
}
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"Connect {details} result: {ErrorCodes.ResultToString(result)}");
return result;
}
private void AddConnection(IConnectionDetails details)
{
lock (CONNECT_LOCK)
{
_connections.Add(details);
}
}
private ulong ConnectRemote(IConnectionDetails details)
{
return ErrorCodes.ERROR_SUCCESS;
}
private ulong ConnectLocal(IConnectionDetails details)
{
try
{
try
{
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, "Stopping instance");
StopInstance(details, DTS.Common.Utilities.Logging.APILogger.Log);
}
catch (Exception ex)
{
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"StopInstance {details} - {ex.Message}");
//29362 Don't re-throw exception if there was no instance to stop - just keep going
if (!(ex is DTS.Common.Utils.Database.SqlServerLocalDbException) ||
(ex as DTS.Common.Utils.Database.SqlServerLocalDbException).Error != DTS.Common.Utils.Database.SqlServerLocalDbException.Errors.LocalDbDoesntExist)
{
throw ex;
}
}
try
{
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, "Deleting instance");
DeleteInstance(details, DTS.Common.Utilities.Logging.APILogger.Log);
}
catch (Exception ex)
{
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"DeleteInstance {details} - {ex.Message}");
//29362 Don't re-throw exception if there was no instance to delete - just keep going
if (!(ex is DTS.Common.Utils.Database.SqlServerLocalDbException) ||
(ex as DTS.Common.Utils.Database.SqlServerLocalDbException).Error != DTS.Common.Utils.Database.SqlServerLocalDbException.Errors.LocalDbDoesntExist)
{
throw ex;
}
}
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, "Creating instance");
CreateInstance(details, DTS.Common.Utilities.Logging.APILogger.Log);
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, "Starting instance");
StartInstance(details, DTS.Common.Utilities.Logging.APILogger.Log);
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, "Attaching databases");
AttachDatabases(details, DTS.Common.Utilities.Logging.APILogger.Log);
}
catch (Exception ex)
{
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"ConnectLocal {details} - {ex.Message}");
return ErrorCodes.ERROR_UNKNOWN;
}
return ErrorCodes.ERROR_SUCCESS;
}
private static readonly object PROCESS_LOCK = new object();
private static readonly StringBuilder sb = new StringBuilder();
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);
}
}
private static readonly StringBuilder sbErrors = new StringBuilder();
/// <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, DTS.Common.Utils.Database.LogDelegate log)
{
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"{command}");
var resultString = string.Empty;
lock (PROCESS_LOCK)
{
sb.Clear();
sbErrors.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 (sbErrors.Length > 0)
{
resultString = sbErrors.ToString();
}
}
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"{resultString}");
return resultString;
}
private static void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(e.Data))
{
sbErrors.AppendLine(e.Data);
}
}
private string ProcessSqlLocalDbCommand(string command, string localDbPath, DTS.Common.Utils.Database.LogDelegate log)
{
if (localDbPath == string.Empty)
{
//SQL Server LocalDb is not installed so display error and go away
throw new FileNotFoundException();
}
var sqlLocalDbExeFileName = localDbPath + "SqlLocalDB.exe";
return SqlCommandProcessor(sqlLocalDbExeFileName, command, log);
}
private void StopInstance(IConnectionDetails details, DTS.Common.Utils.Database.LogDelegate log)
{
var resultString = ProcessSqlLocalDbCommand($"stop {details.InstanceName}", details.SqlDbPath, log);
if (resultString.Length != 0)
{
////29362 If the error is because the instance doesn't exist, throw that type of Exception
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"Stop result: {resultString}");
//if (resultString.Contains(DTS.Common.Strings.Strings.SQLLocalDBInstanceDoesNotExist))
//{
throw new DTS.Common.Utils.Database.SqlServerLocalDbException(DTS.Common.Utils.Database.SqlServerLocalDbException.Errors.LocalDbDoesntExist, resultString);
//}
//else
//{
// throw new Exception(resultString);
//}
}
}
private void DeleteInstance(IConnectionDetails details, DTS.Common.Utils.Database.LogDelegate log)
{
var resultString = ProcessSqlLocalDbCommand($"delete {details.InstanceName}", details.SqlDbPath, log);
if (resultString.Length != 0)
{
////29362 If the error is because the instance doesn't exist, throw that type of Exception
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"Delete result: {resultString}");
//if (resultString.Contains(DTS.Common.Strings.Strings.SQLLocalDBInstanceDoesNotExist))
//{
throw new DTS.Common.Utils.Database.SqlServerLocalDbException(DTS.Common.Utils.Database.SqlServerLocalDbException.Errors.LocalDbDoesntExist, resultString);
//}
//else
//{
// throw new Exception(resultString);
//}
}
}
private void CreateInstance(IConnectionDetails details, DTS.Common.Utils.Database.LogDelegate log)
{
var resultString = ProcessSqlLocalDbCommand($"create {details.InstanceName}", details.SqlDbPath, log);
if (resultString.Length != 0)
{
throw new Exception(resultString);
}
}
private void StartInstance(IConnectionDetails details, DTS.Common.Utils.Database.LogDelegate log)
{
var resultString = ProcessSqlLocalDbCommand($"start {details.InstanceName}", details.SqlDbPath, log);
if (resultString.Length != 0)
{
throw new Exception(resultString);
}
}
private static string BatchCommandProcessor(string batchFileName,
string dbName,
string sqlDbFileName,
string sqlLogFileName,
string fullSqlcmdPath,
DTS.Common.Utils.Database.LogDelegate log)
{
var resultString = string.Empty;
lock (PROCESS_LOCK)
{
sb.Clear();
sbErrors.Clear();
var fi = new FileInfo(batchFileName);
var fi2 = new FileInfo(sqlLogFileName);
var fi3 = new FileInfo(sqlDbFileName);
var process = new Process
{
StartInfo =
{
FileName = fi.FullName,
Arguments = $"{dbName} \"{fi3.FullName}\" \"{fi2.FullName}\" {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 (sbErrors.Length > 0)
{
resultString += sbErrors.ToString();
}
}
return resultString;
}
/// <summary>
/// attaches to a given database
/// throws DbNotAttached exception
/// </summary>
private static void AttachDatabase(IConnectionDetails details, string dbName, DTS.Common.Utils.Database.LogDelegate log)
{
const string SqlCmdExe = "sqlcmd.exe";
var dbFileName = Path.Combine(details.DbFolderPath, dbName) + ".mdf";
var logFileName = Path.Combine(details.DbFolderPath, dbName) + "_log.ldf";
var batchFileName = details.AttachDbsBatPath;
log?.Invoke($"ODBCToolsPath is {details.ODBCToolsPath}");
var fullSqlcmdPath = Path.Combine(details.ODBCToolsPath, SqlCmdExe); //e.g. $"\"C:\\Program Files\\Microsoft SQL Server\\Client SDK\\ODBC\\110\\Tools\\Binn\\sqlcmd.exe\""
if (!File.Exists(fullSqlcmdPath))
{
log?.Invoke($"sqlcmd.exe DOES NOT EXIST at {fullSqlcmdPath}");
}
var resultString = BatchCommandProcessor(batchFileName, dbName, dbFileName, logFileName, $"\"{fullSqlcmdPath}\"", log);
if (resultString.Length != 0)
{
throw new Exception(resultString);
}
}
private void AttachDatabases(IConnectionDetails details, DTS.Common.Utils.Database.LogDelegate log)
{
//Attach the DataPRO database
AttachDatabase(details, details.DbName, log);
//Attach the ISO database
AttachDatabase(details, "ISO", log);
}
internal static ulong GetSqlCommand(IConnectionDetails con, out SqlCommand cmd, string commandText = "")
{
try
{
cmd = new SqlCommand();
cmd.Connection = new SqlConnection(con.GetConnectionString());
cmd.Connection.Open();
cmd.CommandText = commandText;
LogManager.Log(TraceEventType.Information, LogManager.LogEvents.Connections, $"CommandText is {commandText}");
return ErrorCodes.ERROR_SUCCESS;
}
catch (Exception ex)
{
cmd = null;
LogManager.Log(TraceEventType.Error, LogManager.LogEvents.Connections, $"GetSqlCommand, {ex.Message}");
return ErrorCodes.ERROR_UNKNOWN;
}
}
public bool IsUserLoggedIn(IUserDbRecord user, IConnectionDetails details)
{
lock (LOGIN_LOCK)
{
return _loggedInUsers.Exists(item => item.Item1 == user && item.Item2 == details);
}
}
}
}

View File

@@ -0,0 +1,70 @@
namespace DbAPI.Connections
{
public interface IConnectionDetails
{
/// <summary>
/// Whether NTLM Authentication should be used
/// </summary>
bool UseNTLMAuthentication { get; set; }
/// <summary>
/// when connecting to a database with a specific user/pwd, what user to use
/// </summary>
string DbUser { get; set; }
/// <summary>
/// When connecting to a database with a specific user/pwd, what pwd to use
/// </summary>
string DbUserPassword { get; set; }
/// <summary>
/// When connecting to a LocalDB, what the name of the instance should be
/// </summary>
string InstanceName { get; set; }
/// <summary>
/// When connecting to a remote DB, what server the DB is on
/// </summary>
string DbServer { get; set; }
/// <summary>
/// Name of database to connect to
/// </summary>
string DbName { get; set; }
/// <summary>
/// Whether connecting to a LocalDB or a remote DB
/// </summary>
bool UsingCentralizedDb { get; set; }
/// <summary>
/// path to db files if using LocalDB
/// </summary>
string DbFolderPath { get; set; }
/// <summary>
/// path to attach.bat if using LocalDB
/// </summary>
string AttachDbsBatPath { get; set; }
/// <summary>
/// The path to the SQLCMD.EXE that we want to use
/// </summary>
string ODBCToolsPath { get; set; }
/// <summary>
/// a string suitable for SqlClient to connect to
/// </summary>
/// <returns></returns>
string GetConnectionString();
/// <summary>
/// returns a deep copy of connection details
/// </summary>
/// <returns></returns>
IConnectionDetails Clone();
/// <summary>
/// Path to SQL Server when using LocalDB
/// </summary>
string SqlDbPath { get; set; }
/// <summary>
/// holds the code version of the database, what we would use if available
/// </summary>
int ClientDbVersion { get; set; }
/// <summary>
/// holds the version of the database, what we will be using
/// </summary>
int ConnectionDbVersion { get; set; }
}
}

View File

@@ -0,0 +1,62 @@
using DbAPI.User;
using DTS.Common.Interface.Database;
using System;
namespace DbAPI.Connections
{
/// <summary>
/// Connection related functions (Connect, Login, GetLoggedInUsers)
/// </summary>
public interface IConnections
{
/// <summary>
/// Used to retrieve all current connections
/// </summary>
/// <returns>all dbs that were successfully connected to</returns>
IConnectionDetails[] GetActiveConnections();
/// <summary>
/// attempts to login user
/// </summary>
/// <param name="connection">Database user belongs to</param>
/// <param name="user">the DataPRO user to log in</param>
/// <param name="hash">the already hashed and salted password value</param>
/// <param name="userObject">[Output]User instance if login was successful</param>
/// <returns>0 (ERROR_SUCCESS) on success, all other returns are errors.
/// ERROR_LOGINFAILED indicates a failed login while ERROR_UNKNOWN indicates an unexpected error</returns>
ulong LoginUserHash(IConnectionDetails connection, string user, string hash, out IUserDbRecord userObject);
/// <summary>
/// attempts to login user
/// </summary>
/// <param name="connection">Database user belongs to</param>
/// <param name="user">the DataPRO user to log in</param>
/// <param name="password">password belonging to user</param>
/// <param name="userObject">[Output]User instance if login was successful</param>
/// <returns>0 (ERROR_SUCCESS) on success, all other returns are errors.
/// ERROR_LOGINFAILED indicates a failed login while ERROR_UNKNOWN indicates an unexpected error</returns>
ulong LoginUser(IConnectionDetails connection, string user, string password, out IUserDbRecord userObject);
/// <summary>
/// Attempts to connect database
/// </summary>
/// <param name="details">connection details for use in connecting database</param>
/// <returns>returns 0 on success (ERROR_SUCCESS), all other values are errors</returns>
ulong ConnectToDb(IConnectionDetails details);
/// <summary>
/// checks whether a user is currently logged in to the given database
/// </summary>
/// <param name="user">user to check</param>
/// <param name="connection">database user should be logged in to</param>
/// <returns>returns true if user is logged in, otherwise false</returns>
bool IsUserLoggedIn(IUserDbRecord user, IConnectionDetails connection);
/// <summary>
/// returns all logged in users on all databases connected
/// </summary>
/// <returns>all logged in users on all databases connected</returns>
Tuple<IUserDbRecord, IConnectionDetails>[] GetLoggedInUsers();
/// <summary>
/// removes all logged in users and all database connections
/// </summary>
void ClearConnections();
}
}