diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index 1f836d84d5..83fc516f59 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -108,6 +108,9 @@ Microsoft\Data\SqlClient\ColumnEncryptionKeyInfo.cs + + Microsoft\Data\SqlClient\DataSource.cs + Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs @@ -742,7 +745,7 @@ - + @@ -753,8 +756,9 @@ - - + + Microsoft\Data\SqlClient\LocalDB.Windows.cs + @@ -768,7 +772,6 @@ Common\Interop\Windows\kernel32\Interop.GetProcAddress.cs - @@ -793,7 +796,7 @@ - + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDB.Unix.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDB.Unix.cs new file mode 100644 index 0000000000..809b0b69d6 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDB.Unix.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.Data.ProviderBase; +using Microsoft.Data.Common; + +namespace Microsoft.Data.SqlClient +{ + internal class LocalDB + { + internal static string GetLocalDBConnectionString(string localDbInstance) + { + throw ADP.LocalDBNotSUpportedException(); + } + + internal static string GetLocalDBDataSource(string fullServerName, TimeoutTimer timeout) + { + throw ADP.LocalDBNotSUpportedException(); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/LocalDB.uap.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDB.uap.cs similarity index 59% rename from src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/LocalDB.uap.cs rename to src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDB.uap.cs index e764375802..aac8fb76a7 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/LocalDB.uap.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDB.uap.cs @@ -2,7 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -namespace Microsoft.Data.SqlClient.SNI +using System; +using Microsoft.Data.ProviderBase; + +namespace Microsoft.Data.SqlClient { internal class LocalDB { @@ -10,5 +13,10 @@ internal static string GetLocalDBConnectionString(string localDbInstance) { throw new PlatformNotSupportedException(Strings.LocalDBNotSupported); // No Registry support on UAP } + + internal static string GetLocalDBDataSource(string fullServerName, TimeoutTimer timeout) + { + throw new PlatformNotSupportedException(Strings.LocalDBNotSupported); // No Registry support on UAP + } } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDBAPI.Common.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDBAPI.Common.cs deleted file mode 100644 index f30ddf4243..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDBAPI.Common.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Diagnostics; -using System.Globalization; -using System.Runtime.InteropServices; -using System.Text; -using Microsoft.Data.SqlClient; - -namespace Microsoft.Data -{ - internal static partial class LocalDBAPI - { - private static LocalDBFormatMessageDelegate s_localDBFormatMessage = null; - - internal static void ReleaseDLLHandles() - { - s_userInstanceDLLHandle = IntPtr.Zero; - s_localDBFormatMessage = null; - } - - - private static LocalDBFormatMessageDelegate LocalDBFormatMessage - { - get - { - if (s_localDBFormatMessage == null) - { - lock (s_dllLock) - { - if (s_localDBFormatMessage == null) - { - IntPtr functionAddr = LoadProcAddress(); - - if (functionAddr == IntPtr.Zero) - { - // SNI checks for LocalDBFormatMessage during DLL loading, so it is practically impossible to get this error. - int hResult = Marshal.GetLastWin32Error(); - SqlClientEventSource.Log.TryTraceEvent("LocalDBAPI.LocalDBFormatMessage> GetProcAddress for LocalDBFormatMessage error 0x{0}", hResult); - throw CreateLocalDBException(errorMessage: Strings.LocalDB_MethodNotFound); - } - s_localDBFormatMessage = Marshal.GetDelegateForFunctionPointer(functionAddr); - } - } - } - return s_localDBFormatMessage; - } - } - - //This is copy of handle that SNI maintains, so we are responsible for freeing it - therefore there we are not using SafeHandle - private static IntPtr s_userInstanceDLLHandle = IntPtr.Zero; - - private static readonly object s_dllLock = new object(); - - - private const uint const_LOCALDB_TRUNCATE_ERR_MESSAGE = 1;// flag for LocalDBFormatMessage that indicates that message can be truncated if it does not fit in the buffer - private const int const_ErrorMessageBufferSize = 1024; // Buffer size for Local DB error message 1K will be enough for all messages - - - internal static string GetLocalDBMessage(int hrCode) - { - Debug.Assert(hrCode < 0, "HRCode does not indicate error"); - try - { - StringBuilder buffer = new StringBuilder((int)const_ErrorMessageBufferSize); - uint len = (uint)buffer.Capacity; - - - // First try for current culture - int hResult = LocalDBFormatMessage(hrLocalDB: hrCode, dwFlags: const_LOCALDB_TRUNCATE_ERR_MESSAGE, dwLanguageId: (uint)CultureInfo.CurrentCulture.LCID, - buffer: buffer, buflen: ref len); - if (hResult >= 0) - return buffer.ToString(); - else - { - // Message is not available for current culture, try default - buffer = new StringBuilder((int)const_ErrorMessageBufferSize); - len = (uint)buffer.Capacity; - hResult = LocalDBFormatMessage(hrLocalDB: hrCode, dwFlags: const_LOCALDB_TRUNCATE_ERR_MESSAGE, dwLanguageId: 0 /* thread locale with fallback to English */, - buffer: buffer, buflen: ref len); - if (hResult >= 0) - return buffer.ToString(); - else - return string.Format(CultureInfo.CurrentCulture, "{0} (0x{1:X}).", Strings.LocalDB_UnobtainableMessage, hResult); - } - } - catch (SqlException exc) - { - return string.Format(CultureInfo.CurrentCulture, "{0} ({1}).", Strings.LocalDB_UnobtainableMessage, exc.Message); - } - } - - - private static SqlException CreateLocalDBException(string errorMessage, string instance = null, int localDbError = 0, int sniError = 0) - { - Debug.Assert((localDbError == 0) || (sniError == 0), "LocalDB error and SNI error cannot be specified simultaneously"); - Debug.Assert(!string.IsNullOrEmpty(errorMessage), "Error message should not be null or empty"); - SqlErrorCollection collection = new SqlErrorCollection(); - - int errorCode = (localDbError == 0) ? sniError : localDbError; - - if (sniError != 0) - { - string sniErrorMessage = SQL.GetSNIErrorMessage(sniError); - errorMessage = string.Format("{0} (error: {1} - {2})", - errorMessage, sniError, sniErrorMessage); - } - - collection.Add(new SqlError(errorCode, 0, TdsEnums.FATAL_ERROR_CLASS, instance, errorMessage, null, 0)); - - if (localDbError != 0) - collection.Add(new SqlError(errorCode, 0, TdsEnums.FATAL_ERROR_CLASS, instance, GetLocalDBMessage(localDbError), null, 0)); - - SqlException exc = SqlException.CreateException(collection, null); - - exc._doNotReconnect = true; - - return exc; - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDBAPI.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDBAPI.Windows.cs deleted file mode 100644 index 1b4679bfe4..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDBAPI.Windows.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using Microsoft.Data.SqlClient; - -namespace Microsoft.Data -{ - internal static partial class LocalDBAPI - { - private static IntPtr LoadProcAddress() => SafeNativeMethods.GetProcAddress(UserInstanceDLLHandle, "LocalDBFormatMessage"); - - private static IntPtr UserInstanceDLLHandle - { - get - { - if (s_userInstanceDLLHandle == IntPtr.Zero) - { - lock (s_dllLock) - { - if (s_userInstanceDLLHandle == IntPtr.Zero) - { - SNINativeMethodWrapper.SNIQueryInfo(SNINativeMethodWrapper.QTypes.SNI_QUERY_LOCALDB_HMODULE, ref s_userInstanceDLLHandle); - if (s_userInstanceDLLHandle != IntPtr.Zero) - { - SqlClientEventSource.Log.TryTraceEvent("LocalDBAPI.UserInstanceDLLHandle | LocalDB - handle obtained"); - } - else - { - SNINativeMethodWrapper.SNIGetLastError(out SNINativeMethodWrapper.SNI_Error sniError); - throw CreateLocalDBException(errorMessage: StringsHelper.GetString("LocalDB_FailedGetDLLHandle"), sniError: (int)sniError.sniError); - } - } - } - } - return s_userInstanceDLLHandle; - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDBAPI.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDBAPI.cs index ba2371c232..af3f9e0fc5 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDBAPI.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/LocalDBAPI.cs @@ -13,7 +13,6 @@ internal static partial class LocalDBAPI private const string LocalDbPrefix = @"(localdb)\"; private const string LocalDbPrefix_NP = @"np:\\.\pipe\LOCALDB#"; - [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Unicode)] private delegate int LocalDBFormatMessageDelegate(int hrLocalDB, uint dwFlags, uint dwLanguageId, StringBuilder buffer, ref uint buflen); @@ -38,7 +37,6 @@ internal static string GetLocalDbInstanceNameFromServerName(string serverName) { return input.ToString(); } - } return null; } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/LocalDB.Unix.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/LocalDB.Unix.cs deleted file mode 100644 index a9b235d47c..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/LocalDB.Unix.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; - -namespace Microsoft.Data.SqlClient.SNI -{ - internal class LocalDB - { - internal static string GetLocalDBConnectionString(string localDbInstance) - { - throw new PlatformNotSupportedException(Strings.LocalDBNotSupported); // LocalDB is not available for Unix and hence it cannot be supported. - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/LocalDB.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/LocalDB.Windows.cs deleted file mode 100644 index 68eaa25486..0000000000 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/LocalDB.Windows.cs +++ /dev/null @@ -1,260 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Runtime.InteropServices; -using System.Text; -using Microsoft.Win32; -using Microsoft.Win32.SafeHandles; - -namespace Microsoft.Data.SqlClient.SNI -{ - internal sealed class LocalDB - { - private static readonly LocalDB Instance = new LocalDB(); - - //HKEY_LOCAL_MACHINE - private const string LocalDBInstalledVersionRegistryKey = "SOFTWARE\\Microsoft\\Microsoft SQL Server Local DB\\Installed Versions\\"; - - private const string InstanceAPIPathValueName = "InstanceAPIPath"; - - private const string ProcLocalDBStartInstance = "LocalDBStartInstance"; - - private const int MAX_LOCAL_DB_CONNECTION_STRING_SIZE = 260; - - private IntPtr _startInstanceHandle = IntPtr.Zero; - - // Local Db api doc https://msdn.microsoft.com/en-us/library/hh217143.aspx - // HRESULT LocalDBStartInstance( [Input ] PCWSTR pInstanceName, [Input ] DWORD dwFlags,[Output] LPWSTR wszSqlConnection,[Input/Output] LPDWORD lpcchSqlConnection); - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - internal delegate int LocalDBStartInstance( - [In] [MarshalAs(UnmanagedType.LPWStr)] string localDBInstanceName, - [In] int flags, - [Out] [MarshalAs(UnmanagedType.LPWStr)] StringBuilder sqlConnectionDataSource, - [In, Out]ref int bufferLength); - - private LocalDBStartInstance localDBStartInstanceFunc = null; - - private volatile SafeLibraryHandle _sqlUserInstanceLibraryHandle; - - private LocalDB() { } - - internal static string GetLocalDBConnectionString(string localDbInstance) => - Instance.LoadUserInstanceDll() ? Instance.GetConnectionString(localDbInstance) : null; - - internal static IntPtr GetProcAddress(string functionName) => - Instance.LoadUserInstanceDll() ? Interop.Kernel32.GetProcAddress(LocalDB.Instance._sqlUserInstanceLibraryHandle, functionName) : IntPtr.Zero; - - private string GetConnectionString(string localDbInstance) - { - StringBuilder localDBConnectionString = new StringBuilder(MAX_LOCAL_DB_CONNECTION_STRING_SIZE + 1); - int sizeOfbuffer = localDBConnectionString.Capacity; - localDBStartInstanceFunc(localDbInstance, 0, localDBConnectionString, ref sizeOfbuffer); - return localDBConnectionString.ToString(); - } - - internal enum LocalDBErrorState - { - NO_INSTALLATION, INVALID_CONFIG, NO_SQLUSERINSTANCEDLL_PATH, INVALID_SQLUSERINSTANCEDLL_PATH, NONE - } - - internal static uint MapLocalDBErrorStateToCode(LocalDBErrorState errorState) - { - switch (errorState) - { - case LocalDBErrorState.NO_INSTALLATION: - return SNICommon.LocalDBNoInstallation; - case LocalDBErrorState.INVALID_CONFIG: - return SNICommon.LocalDBInvalidConfig; - case LocalDBErrorState.NO_SQLUSERINSTANCEDLL_PATH: - return SNICommon.LocalDBNoSqlUserInstanceDllPath; - case LocalDBErrorState.INVALID_SQLUSERINSTANCEDLL_PATH: - return SNICommon.LocalDBInvalidSqlUserInstanceDllPath; - case LocalDBErrorState.NONE: - return 0; - default: - return SNICommon.LocalDBInvalidConfig; - } - } - - internal static string MapLocalDBErrorStateToErrorMessage(LocalDBErrorState errorState) - { - switch (errorState) - { - case LocalDBErrorState.NO_INSTALLATION: - return Strings.SNI_ERROR_52; - case LocalDBErrorState.INVALID_CONFIG: - return Strings.SNI_ERROR_53; - case LocalDBErrorState.NO_SQLUSERINSTANCEDLL_PATH: - return Strings.SNI_ERROR_54; - case LocalDBErrorState.INVALID_SQLUSERINSTANCEDLL_PATH: - return Strings.SNI_ERROR_55; - case LocalDBErrorState.NONE: - return Strings.SNI_ERROR_50; - default: - return Strings.SNI_ERROR_53; - } - } - - /// - /// Loads the User Instance dll. - /// - private bool LoadUserInstanceDll() - { - using (TrySNIEventScope.Create(nameof(LocalDB))) - { - // Check in a non thread-safe way if the handle is already set for performance. - if (_sqlUserInstanceLibraryHandle != null) - { - return true; - } - - lock (this) - { - if (_sqlUserInstanceLibraryHandle != null) - { - return true; - } - //Get UserInstance Dll path - LocalDBErrorState registryQueryErrorState; - - // Get the LocalDB instance dll path from the registry - string dllPath = GetUserInstanceDllPath(out registryQueryErrorState); - - // If there was no DLL path found, then there is an error. - if (dllPath == null) - { - SNILoadHandle.SingletonInstance.LastError = new SNIError(SNIProviders.INVALID_PROV, 0, MapLocalDBErrorStateToCode(registryQueryErrorState), MapLocalDBErrorStateToErrorMessage(registryQueryErrorState)); - SqlClientEventSource.Log.TrySNITraceEvent(nameof(LocalDB), EventType.ERR, "User instance DLL path is null."); - return false; - } - - // In case the registry had an empty path for dll - if (string.IsNullOrWhiteSpace(dllPath)) - { - SNILoadHandle.SingletonInstance.LastError = new SNIError(SNIProviders.INVALID_PROV, 0, SNICommon.LocalDBInvalidSqlUserInstanceDllPath, Strings.SNI_ERROR_55); - SqlClientEventSource.Log.TrySNITraceEvent(nameof(LocalDB), EventType.ERR, "User instance DLL path is invalid. DLL path = {0}", dllPath); - return false; - } - - // Load the dll - SafeLibraryHandle libraryHandle = Interop.Kernel32.LoadLibraryExW(dllPath.Trim(), IntPtr.Zero, 0); - - if (libraryHandle.IsInvalid) - { - SNILoadHandle.SingletonInstance.LastError = new SNIError(SNIProviders.INVALID_PROV, 0, SNICommon.LocalDBFailedToLoadDll, Strings.SNI_ERROR_56); - SqlClientEventSource.Log.TrySNITraceEvent(nameof(LocalDB), EventType.ERR, "Library Handle is invalid. Could not load the dll."); - libraryHandle.Dispose(); - return false; - } - - // Load the procs from the DLLs - _startInstanceHandle = Interop.Kernel32.GetProcAddress(libraryHandle, ProcLocalDBStartInstance); - - if (_startInstanceHandle == IntPtr.Zero) - { - SNILoadHandle.SingletonInstance.LastError = new SNIError(SNIProviders.INVALID_PROV, 0, SNICommon.LocalDBBadRuntime, Strings.SNI_ERROR_57); - SqlClientEventSource.Log.TrySNITraceEvent(nameof(LocalDB), EventType.ERR, "Was not able to load the PROC from DLL. Bad Runtime."); - libraryHandle.Dispose(); - return false; - } - - // Set the delegate the invoke. - localDBStartInstanceFunc = (LocalDBStartInstance)Marshal.GetDelegateForFunctionPointer(_startInstanceHandle, typeof(LocalDBStartInstance)); - - if (localDBStartInstanceFunc == null) - { - SNILoadHandle.SingletonInstance.LastError = new SNIError(SNIProviders.INVALID_PROV, 0, SNICommon.LocalDBBadRuntime, Strings.SNI_ERROR_57); - libraryHandle.Dispose(); - _startInstanceHandle = IntPtr.Zero; - return false; - } - - _sqlUserInstanceLibraryHandle = libraryHandle; - SqlClientEventSource.Log.TrySNITraceEvent(nameof(LocalDB), EventType.INFO, "User Instance DLL was loaded successfully."); - return true; - } - } - } - - /// - /// Retrieves the part of the sqlUserInstance.dll from the registry - /// - /// In case the dll path is not found, the error is set here. - /// - private string GetUserInstanceDllPath(out LocalDBErrorState errorState) - { - using (TrySNIEventScope.Create(nameof(LocalDB))) - { - string dllPath = null; - using (RegistryKey key = Registry.LocalMachine.OpenSubKey(LocalDBInstalledVersionRegistryKey)) - { - if (key == null) - { - errorState = LocalDBErrorState.NO_INSTALLATION; - SqlClientEventSource.Log.TrySNITraceEvent(nameof(LocalDB), EventType.ERR, "No installation found."); - return null; - } - - Version zeroVersion = new Version(); - - Version latestVersion = zeroVersion; - - foreach (string subKey in key.GetSubKeyNames()) - { - Version currentKeyVersion; - - if (!Version.TryParse(subKey, out currentKeyVersion)) - { - errorState = LocalDBErrorState.INVALID_CONFIG; - SqlClientEventSource.Log.TrySNITraceEvent(nameof(LocalDB), EventType.ERR, "Invalid Configuration."); - return null; - } - - if (latestVersion.CompareTo(currentKeyVersion) < 0) - { - latestVersion = currentKeyVersion; - } - } - - // If no valid versions are found, then error out - if (latestVersion.Equals(zeroVersion)) - { - errorState = LocalDBErrorState.INVALID_CONFIG; - SqlClientEventSource.Log.TrySNITraceEvent(nameof(LocalDB), EventType.ERR, "Invalid Configuration."); - return null; - } - - // Use the latest version to get the DLL path - using (RegistryKey latestVersionKey = key.OpenSubKey(latestVersion.ToString())) - { - - object instanceAPIPathRegistryObject = latestVersionKey.GetValue(InstanceAPIPathValueName); - - if (instanceAPIPathRegistryObject == null) - { - errorState = LocalDBErrorState.NO_SQLUSERINSTANCEDLL_PATH; - SqlClientEventSource.Log.TrySNITraceEvent(nameof(LocalDB), EventType.ERR, "No SQL user instance DLL. Instance API Path Registry Object Error."); - return null; - } - - RegistryValueKind valueKind = latestVersionKey.GetValueKind(InstanceAPIPathValueName); - - if (valueKind != RegistryValueKind.String) - { - errorState = LocalDBErrorState.INVALID_SQLUSERINSTANCEDLL_PATH; - SqlClientEventSource.Log.TrySNITraceEvent(nameof(LocalDB), EventType.ERR, "Invalid SQL user instance DLL path. Registry value kind mismatch."); - return null; - } - - dllPath = (string)instanceAPIPathRegistryObject; - - errorState = LocalDBErrorState.NONE; - return dllPath; - } - } - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNICommon.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNICommon.cs index b92746098f..fc8adb003c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNICommon.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNICommon.cs @@ -119,14 +119,7 @@ internal class SNICommon internal const int MultiSubnetFailoverWithInstanceSpecified = 48; internal const int MultiSubnetFailoverWithNonTcpProtocol = 49; internal const int MaxErrorValue = 50157; - internal const int LocalDBNoInstanceName = 51; - internal const int LocalDBNoInstallation = 52; - internal const int LocalDBInvalidConfig = 53; - internal const int LocalDBNoSqlUserInstanceDllPath = 54; - internal const int LocalDBInvalidSqlUserInstanceDllPath = 55; - internal const int LocalDBFailedToLoadDll = 56; - internal const int LocalDBBadRuntime = 57; - + /// /// We only validate Server name in Certificate to match with "targetServerName". /// Certificate validation and chain trust validations are done by SSLStream class [System.Net.Security.SecureChannel.VerifyRemoteCertificate method] diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIProxy.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIProxy.cs index 8f101b7bdf..82a2c408ee 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIProxy.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIProxy.cs @@ -144,6 +144,7 @@ private static bool IsErrorStatus(SecurityStatusPalErrorCode errorCode) /// Support TDS8.0 /// Used for the HostName in certificate /// Used for the path to the Server Certificate + /// DataSource deatils /// SNI handle internal static SNIHandle CreateConnectionHandle( string fullServerName, @@ -160,26 +161,10 @@ internal static SNIHandle CreateConnectionHandle( ref SQLDNSInfo pendingDNSInfo, bool tlsFirst, string hostNameInCertificate, - string serverCertificateFilename) + string serverCertificateFilename, + DataSource details) { instanceName = new byte[1]; - - bool errorWithLocalDBProcessing; - string localDBDataSource = GetLocalDBDataSource(fullServerName, out errorWithLocalDBProcessing); - - if (errorWithLocalDBProcessing) - { - return null; - } - // If a localDB Data source is available, we need to use it. - fullServerName = localDBDataSource ?? fullServerName; - - DataSource details = DataSource.ParseServerName(fullServerName); - if (details == null) - { - return null; - } - SNIHandle sniHandle = null; switch (details._connectionProtocol) { @@ -366,401 +351,5 @@ internal SNIError GetLastError() { return SNILoadHandle.SingletonInstance.LastError; } - - /// - /// Gets the Local db Named pipe data source if the input is a localDB server. - /// - /// The data source - /// Set true when an error occurred while getting LocalDB up - /// - private static string GetLocalDBDataSource(string fullServerName, out bool error) - { - string localDBConnectionString = null; - string localDBInstance = DataSource.GetLocalDBInstance(fullServerName, out bool isBadLocalDBDataSource); - - if (isBadLocalDBDataSource) - { - error = true; - return null; - } - - else if (!string.IsNullOrEmpty(localDBInstance)) - { - // We have successfully received a localDBInstance which is valid. - Debug.Assert(!string.IsNullOrWhiteSpace(localDBInstance), "Local DB Instance name cannot be empty."); - localDBConnectionString = LocalDB.GetLocalDBConnectionString(localDBInstance); - - if (fullServerName == null) - { - // The Last error is set in LocalDB.GetLocalDBConnectionString. We don't need to set Last here. - error = true; - return null; - } - } - error = false; - return localDBConnectionString; - } - } - - internal class DataSource - { - private const char CommaSeparator = ','; - private const char SemiColon = ':'; - private const char BackSlashCharacter = '\\'; - - private const string DefaultHostName = "localhost"; - private const string DefaultSqlServerInstanceName = "mssqlserver"; - private const string PipeBeginning = @"\\"; - private const string Slash = @"/"; - private const string PipeToken = "pipe"; - private const string LocalDbHost = "(localdb)"; - private const string LocalDbHost_NP = @"np:\\.\pipe\LOCALDB#"; - private const string NamedPipeInstanceNameHeader = "mssql$"; - private const string DefaultPipeName = "sql\\query"; - - internal enum Protocol { TCP, NP, None, Admin }; - - internal Protocol _connectionProtocol = Protocol.None; - - /// - /// Provides the HostName of the server to connect to for TCP protocol. - /// This information is also used for finding the SPN of SqlServer - /// - internal string ServerName { get; private set; } - - /// - /// Provides the port on which the TCP connection should be made if one was specified in Data Source - /// - internal int Port { get; private set; } = -1; - - /// - /// Provides the inferred Instance Name from Server Data Source - /// - internal string InstanceName { get; private set; } - - /// - /// Provides the pipe name in case of Named Pipes - /// - internal string PipeName { get; private set; } - - /// - /// Provides the HostName to connect to in case of Named pipes Data Source - /// - internal string PipeHostName { get; private set; } - - private string _workingDataSource; - private string _dataSourceAfterTrimmingProtocol; - - internal bool IsBadDataSource { get; private set; } = false; - - internal bool IsSsrpRequired { get; private set; } = false; - - private DataSource(string dataSource) - { - // Remove all whitespaces from the datasource and all operations will happen on lower case. - _workingDataSource = dataSource.Trim().ToLowerInvariant(); - - int firstIndexOfColon = _workingDataSource.IndexOf(SemiColon); - - PopulateProtocol(); - - _dataSourceAfterTrimmingProtocol = (firstIndexOfColon > -1) && _connectionProtocol != Protocol.None - ? _workingDataSource.Substring(firstIndexOfColon + 1).Trim() : _workingDataSource; - - if (_dataSourceAfterTrimmingProtocol.Contains(Slash)) // Pipe paths only allow back slashes - { - if (_connectionProtocol == Protocol.None) - ReportSNIError(SNIProviders.INVALID_PROV); - else if (_connectionProtocol == Protocol.NP) - ReportSNIError(SNIProviders.NP_PROV); - else if (_connectionProtocol == Protocol.TCP) - ReportSNIError(SNIProviders.TCP_PROV); - } - } - - private void PopulateProtocol() - { - string[] splitByColon = _workingDataSource.Split(SemiColon); - - if (splitByColon.Length <= 1) - { - _connectionProtocol = Protocol.None; - } - else - { - // We trim before switching because " tcp : server , 1433 " is a valid data source - switch (splitByColon[0].Trim()) - { - case TdsEnums.TCP: - _connectionProtocol = Protocol.TCP; - break; - case TdsEnums.NP: - _connectionProtocol = Protocol.NP; - break; - case TdsEnums.ADMIN: - _connectionProtocol = Protocol.Admin; - break; - default: - // None of the supported protocols were found. This may be a IPv6 address - _connectionProtocol = Protocol.None; - break; - } - } - } - - // LocalDbInstance name always starts with (localdb) - // possible scenarios: - // (localdb)\ - // or (localdb)\. which goes to default localdb - // or (localdb)\.\ - internal static string GetLocalDBInstance(string dataSource, out bool error) - { - string instanceName = null; - // ReadOnlySpan is not supported in netstandard 2.0, but installing System.Memory solves the issue - ReadOnlySpan input = dataSource.AsSpan().TrimStart(); - error = false; - // NetStandard 2.0 does not support passing a string to ReadOnlySpan - if (input.StartsWith(LocalDbHost.AsSpan().Trim(), StringComparison.InvariantCultureIgnoreCase)) - { - // When netcoreapp support for netcoreapp2.1 is dropped these slice calls could be converted to System.Range\System.Index - // Such ad input = input[1..]; - input = input.Slice(LocalDbHost.Length); - if (!input.IsEmpty && input[0] == BackSlashCharacter) - { - input = input.Slice(1); - } - if (!input.IsEmpty) - { - instanceName = input.Trim().ToString(); - } - else - { - SNILoadHandle.SingletonInstance.LastError = new SNIError(SNIProviders.INVALID_PROV, 0, SNICommon.LocalDBNoInstanceName, Strings.SNI_ERROR_51); - error = true; - } - } - else if (input.StartsWith(LocalDbHost_NP.AsSpan().Trim(), StringComparison.InvariantCultureIgnoreCase)) - { - instanceName = input.Trim().ToString(); - } - - return instanceName; - } - - internal static DataSource ParseServerName(string dataSource) - { - DataSource details = new DataSource(dataSource); - - if (details.IsBadDataSource) - { - return null; - } - - if (details.InferNamedPipesInformation()) - { - return details; - } - - if (details.IsBadDataSource) - { - return null; - } - - if (details.InferConnectionDetails()) - { - return details; - } - - return null; - } - - private void InferLocalServerName() - { - // If Server name is empty or localhost, then use "localhost" - if (string.IsNullOrEmpty(ServerName) || IsLocalHost(ServerName) || - (Environment.MachineName.Equals(ServerName, StringComparison.CurrentCultureIgnoreCase) && - _connectionProtocol == Protocol.Admin)) - { - // For DAC use "localhost" instead of the server name. - ServerName = DefaultHostName; - } - } - - private bool InferConnectionDetails() - { - string[] tokensByCommaAndSlash = _dataSourceAfterTrimmingProtocol.Split(BackSlashCharacter, CommaSeparator); - ServerName = tokensByCommaAndSlash[0].Trim(); - - int commaIndex = _dataSourceAfterTrimmingProtocol.IndexOf(CommaSeparator); - - int backSlashIndex = _dataSourceAfterTrimmingProtocol.IndexOf(BackSlashCharacter); - - // Check the parameters. The parameters are Comma separated in the Data Source. The parameter we really care about is the port - // If Comma exists, the try to get the port number - if (commaIndex > -1) - { - string parameter = backSlashIndex > -1 - ? ((commaIndex > backSlashIndex) ? tokensByCommaAndSlash[2].Trim() : tokensByCommaAndSlash[1].Trim()) - : tokensByCommaAndSlash[1].Trim(); - - // Bad Data Source like "server, " - if (string.IsNullOrEmpty(parameter)) - { - ReportSNIError(SNIProviders.INVALID_PROV); - return false; - } - - // For Tcp and Only Tcp are parameters allowed. - if (_connectionProtocol == Protocol.None) - { - _connectionProtocol = Protocol.TCP; - } - else if (_connectionProtocol != Protocol.TCP) - { - // Parameter has been specified for non-TCP protocol. This is not allowed. - ReportSNIError(SNIProviders.INVALID_PROV); - return false; - } - - int port; - if (!int.TryParse(parameter, out port)) - { - ReportSNIError(SNIProviders.TCP_PROV); - return false; - } - - // If the user explicitly specified a invalid port in the connection string. - if (port < 1) - { - ReportSNIError(SNIProviders.TCP_PROV); - return false; - } - - Port = port; - } - // Instance Name Handling. Only if we found a '\' and we did not find a port in the Data Source - else if (backSlashIndex > -1) - { - // This means that there will not be any part separated by comma. - InstanceName = tokensByCommaAndSlash[1].Trim(); - - if (string.IsNullOrWhiteSpace(InstanceName)) - { - ReportSNIError(SNIProviders.INVALID_PROV); - return false; - } - - if (DefaultSqlServerInstanceName.Equals(InstanceName)) - { - ReportSNIError(SNIProviders.INVALID_PROV); - return false; - } - - IsSsrpRequired = true; - } - - InferLocalServerName(); - - return true; - } - - private void ReportSNIError(SNIProviders provider) - { - SNILoadHandle.SingletonInstance.LastError = new SNIError(provider, 0, SNICommon.InvalidConnStringError, Strings.SNI_ERROR_25); - IsBadDataSource = true; - } - - private bool InferNamedPipesInformation() - { - // If we have a datasource beginning with a pipe or we have already determined that the protocol is Named Pipe - if (_dataSourceAfterTrimmingProtocol.StartsWith(PipeBeginning, StringComparison.Ordinal) || _connectionProtocol == Protocol.NP) - { - // If the data source is "np:servername" - if (!_dataSourceAfterTrimmingProtocol.Contains(PipeBeginning)) - { - PipeHostName = ServerName = _dataSourceAfterTrimmingProtocol; - InferLocalServerName(); - PipeName = SNINpHandle.DefaultPipePath; - return true; - } - - try - { - string[] tokensByBackSlash = _dataSourceAfterTrimmingProtocol.Split(BackSlashCharacter); - - // The datasource is of the format \\host\pipe\sql\query [0]\[1]\[2]\[3]\[4]\[5] - // It would at least have 6 parts. - // Another valid Sql named pipe for an named instance is \\.\pipe\MSSQL$MYINSTANCE\sql\query - if (tokensByBackSlash.Length < 6) - { - ReportSNIError(SNIProviders.NP_PROV); - return false; - } - - string host = tokensByBackSlash[2]; - - if (string.IsNullOrEmpty(host)) - { - ReportSNIError(SNIProviders.NP_PROV); - return false; - } - - //Check if the "pipe" keyword is the first part of path - if (!PipeToken.Equals(tokensByBackSlash[3])) - { - ReportSNIError(SNIProviders.NP_PROV); - return false; - } - - if (tokensByBackSlash[4].StartsWith(NamedPipeInstanceNameHeader, StringComparison.Ordinal)) - { - InstanceName = tokensByBackSlash[4].Substring(NamedPipeInstanceNameHeader.Length); - } - - StringBuilder pipeNameBuilder = new StringBuilder(); - - for (int i = 4; i < tokensByBackSlash.Length - 1; i++) - { - pipeNameBuilder.Append(tokensByBackSlash[i]); - pipeNameBuilder.Append(Path.DirectorySeparatorChar); - } - // Append the last part without a "/" - pipeNameBuilder.Append(tokensByBackSlash[tokensByBackSlash.Length - 1]); - PipeName = pipeNameBuilder.ToString(); - - if (string.IsNullOrWhiteSpace(InstanceName) && !DefaultPipeName.Equals(PipeName)) - { - InstanceName = PipeToken + PipeName; - } - - ServerName = IsLocalHost(host) ? Environment.MachineName : host; - // Pipe hostname is the hostname after leading \\ which should be passed down as is to open Named Pipe. - // For Named Pipes the ServerName makes sense for SPN creation only. - PipeHostName = host; - } - catch (UriFormatException) - { - ReportSNIError(SNIProviders.NP_PROV); - return false; - } - - // DataSource is something like "\\pipename" - if (_connectionProtocol == Protocol.None) - { - _connectionProtocol = Protocol.NP; - } - else if (_connectionProtocol != Protocol.NP) - { - // In case the path began with a "\\" and protocol was not Named Pipes - ReportSNIError(SNIProviders.NP_PROV); - return false; - } - return true; - } - return false; - } - - private static bool IsLocalHost(string serverName) - => ".".Equals(serverName) || "(local)".Equals(serverName) || "localhost".Equals(serverName); } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index 0b745ae01e..4e44b22339 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -10,6 +10,7 @@ using System.Globalization; using System.Net.Http.Headers; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Security; using System.Text; using System.Threading; @@ -1913,14 +1914,40 @@ private void AttemptOneLogin( { SqlClientEventSource.Log.TryAdvancedTraceEvent(" {0}, timout={1}[msec], server={2}", ObjectID, timeout.MillisecondsRemaining, serverInfo.ExtendedServerName); RoutingInfo = null; // forget routing information + string localDBDataSource = null; + DataSource details = null; + if (ConnectionOptions.LocalDBInstance != null) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + //do not get np result if data source is already np + string[] splitByColon = serverInfo.UserServerName.Split(':'); + if (!splitByColon[0].Trim().Equals("np", StringComparison.CurrentCultureIgnoreCase)) + { + localDBDataSource = LocalDB.GetLocalDBDataSource(serverInfo.UserServerName, timeout); + //re-writing serverinfo to make NP as servername + ServerInfo original = serverInfo; + serverInfo = new ServerInfo(ConnectionOptions, localDBDataSource, original.ServerSPN); + serverInfo.SetDerivedNames(original.UserProtocol, localDBDataSource); + } + } + else + { + throw new NotSupportedException(); + } + } + string serverName = localDBDataSource ?? serverInfo.ExtendedServerName; + details = DataSource.ParseServerName(serverName); + _parser._physicalStateObj.SniContext = SniContext.Snix_Connect; _parser.Connect(serverInfo, this, - timeout.LegacyTimerExpire, + timeout, ConnectionOptions, - withFailover); + withFailover, + details); _timeoutErrorInternal.EndPhase(SqlConnectionTimeoutErrorPhase.ConsumePreLoginHandshake); _timeoutErrorInternal.SetAndBeginPhase(SqlConnectionTimeoutErrorPhase.LoginBegin); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index 8c16690149..28562b2a94 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -16,6 +16,7 @@ using System.Threading.Tasks; using System.Xml; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; using Microsoft.Data.Sql; using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.Server; @@ -361,9 +362,10 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) internal void Connect( ServerInfo serverInfo, SqlInternalConnectionTds connHandler, - long timerExpire, + TimeoutTimer timerExpire, SqlConnectionString connectionOptions, - bool withFailover) + bool withFailover, + DataSource details) { SqlConnectionEncryptOption encrypt = connectionOptions.Encrypt; bool isTlsFirst = (encrypt == SqlConnectionEncryptOption.Strict); @@ -454,6 +456,7 @@ internal void Connect( FQDNforDNSCache, ref _connHandler.pendingSQLDNSObject, serverInfo.ServerSPN, + details, integratedSecurity || authType == SqlAuthenticationMethod.ActiveDirectoryIntegrated, isTlsFirst, hostNameInCertificate, @@ -487,7 +490,7 @@ internal void Connect( } _state = TdsParserState.OpenNotLoggedIn; _physicalStateObj.SniContext = SniContext.Snix_PreLoginBeforeSuccessfulWrite; - _physicalStateObj.TimeoutTime = timerExpire; + _physicalStateObj.TimeoutTime = timerExpire.LegacyTimerExpire; bool marsCapable = false; @@ -551,6 +554,7 @@ internal void Connect( FQDNforDNSCache, ref _connHandler.pendingSQLDNSObject, serverInfo.ServerSPN, + details, integratedSecurity, isTlsFirst, hostNameInCertificate, @@ -1530,12 +1534,6 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) // errorMessage = SQL.GetSNIErrorMessage((int)details.sniErrorNumber); - // If its a LocalDB error, then nativeError actually contains a LocalDB-specific error code, not a win32 error code - if (details.sniErrorNumber == (int)SNINativeMethodWrapper.SniSpecialErrors.LocalDBErrorCode) - { - errorMessage += LocalDBAPI.GetLocalDBMessage((int)details.nativeError); - win32ErrorCode = 0; - } } } errorMessage = string.Format("{0} (provider: {1}, error: {2} - {3})", diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index 1f15345167..2fc9bd2e50 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; namespace Microsoft.Data.SqlClient { @@ -198,7 +199,7 @@ private void ResetCancelAndProcessAttention() internal abstract void CreatePhysicalSNIHandle( string serverName, - long timerExpire, + TimeoutTimer timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, bool flushCache, @@ -208,6 +209,7 @@ internal abstract void CreatePhysicalSNIHandle( string cachedFQDN, ref SQLDNSInfo pendingDNSInfo, string serverSPN, + DataSource dataSourceDetails, bool isIntegratedSecurity = false, bool tlsFirst = false, string hostNameInCertificate = "", diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs index 9665d8f188..6f3576ac4f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs @@ -13,6 +13,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; namespace Microsoft.Data.SqlClient.SNI { @@ -82,7 +83,7 @@ protected override uint SNIPacketGetData(PacketHandle packet, byte[] inBuff, ref internal override void CreatePhysicalSNIHandle( string serverName, - long timerExpire, + TimeoutTimer timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, bool flushCache, @@ -92,15 +93,17 @@ internal override void CreatePhysicalSNIHandle( string cachedFQDN, ref SQLDNSInfo pendingDNSInfo, string serverSPN, + DataSource details, bool isIntegratedSecurity, bool tlsFirst, string hostNameInCertificate, string serverCertificateFilename) { - SNIHandle? sessionHandle = SNIProxy.CreateConnectionHandle(serverName, timerExpire, out instanceName, ref spnBuffer, serverSPN, - flushCache, async, parallel, isIntegratedSecurity, iPAddressPreference, cachedFQDN, ref pendingDNSInfo, tlsFirst, - hostNameInCertificate, serverCertificateFilename); - + SNIHandle? sessionHandle = SNIProxy.CreateConnectionHandle(serverName, timerExpire.LegacyTimerExpire, out instanceName, ref spnBuffer, serverSPN, + flushCache, async, parallel, isIntegratedSecurity, iPAddressPreference, cachedFQDN, ref pendingDNSInfo, tlsFirst, + hostNameInCertificate, serverCertificateFilename, details); + + if (sessionHandle is not null) { _sessionHandle = sessionHandle; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs index bf8337cacb..50c32741d2 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs @@ -5,12 +5,13 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Net; using System.Runtime.InteropServices; using System.Security.Authentication; +using System.Text; using System.Threading.Tasks; using Microsoft.Data.Common; -using System.Net; -using System.Text; +using Microsoft.Data.ProviderBase; namespace Microsoft.Data.SqlClient { @@ -140,7 +141,7 @@ private SNINativeMethodWrapper.ConsumerInfo CreateConsumerInfo(bool async) internal override void CreatePhysicalSNIHandle( string serverName, - long timerExpire, + TimeoutTimer timerExpire, out byte[] instanceName, ref byte[][] spnBuffer, bool flushCache, @@ -150,6 +151,7 @@ internal override void CreatePhysicalSNIHandle( string cachedFQDN, ref SQLDNSInfo pendingDNSInfo, string serverSPN, + DataSource dataSource, bool isIntegratedSecurity, bool tlsFirst, string hostNameInCertificate, @@ -178,13 +180,13 @@ internal override void CreatePhysicalSNIHandle( // Translate to SNI timeout values (Int32 milliseconds) long timeout; - if (long.MaxValue == timerExpire) + if (long.MaxValue == timerExpire.LegacyTimerExpire) { timeout = int.MaxValue; } else { - timeout = ADP.TimerRemainingMilliseconds(timerExpire); + timeout = ADP.TimerRemainingMilliseconds(timerExpire.LegacyTimerExpire); if (timeout > int.MaxValue) { timeout = int.MaxValue; diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs index cf900c4553..30a9384826 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.Designer.cs @@ -1158,6 +1158,15 @@ internal static string LocalDB_MethodNotFound { } } + /// + /// Looks up a localized string similar to SQLUserInstanceDLL is not supported on current plaform. Install SqlLocalDB as an alternate option to connect to LocalDB.. + /// + internal static string LocalDB_SQLUserInstanceDLL { + get { + return ResourceManager.GetString("LocalDB_SQLUserInstanceDLL", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot obtain Local Database Runtime error message. /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx index 7f52b2556f..1cf66028fd 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/Strings.resx @@ -1950,4 +1950,7 @@ Cannot set the AccessTokenCallback property if 'Authentication=Active Directory Default' has been specified in the connection string. + + SQLUserInstanceDLL is not supported on current plaform. Install SqlLocalDB as an alternate option to connect to LocalDB. + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 675dd57fe6..c40c02e087 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -185,6 +185,9 @@ Microsoft\Data\SqlClient\DataClassification\SensitivityClassification.cs + + Microsoft\Data\SqlClient\DataSource.cs + Microsoft\Data\SqlClient\DisposableTemporaryOnStack.cs @@ -206,6 +209,9 @@ Microsoft\Data\SqlClient\LocalAppContextSwitches.cs + + Microsoft\Data\SqlClient\LocalDB.Windows.cs + Microsoft\Data\SqlClient\NoneAttestationEnclaveProvider.cs @@ -599,6 +605,21 @@ Resources\ResDescriptionAttribute.cs + + Common\Interop\Windows\Interop.Libraries.cs + + + Common\Interop\Windows\kernel32\Interop.LoadLibraryEx.cs + + + Common\CoreLib\Microsoft\Win32\SafeHandles\SafeLibraryHandle.cs + + + Common\Interop\Windows\kernel32\Interop.FreeLibrary.cs + + + Common\Interop\Windows\kernel32\Interop.GetProcAddress.cs + @@ -625,7 +646,6 @@ - @@ -737,4 +757,4 @@ - \ No newline at end of file + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs index b9af5849c4..c516df7c54 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs @@ -38,18 +38,10 @@ internal static class SNINativeMethodWrapper internal const int InternalExceptionError = 35; internal const int ConnOpenFailedError = 40; internal const int ErrorSpnLookup = 44; - internal const int LocalDBErrorCode = 50; internal const int MultiSubnetFailoverWithMoreThan64IPs = 47; internal const int MultiSubnetFailoverWithInstanceSpecified = 48; internal const int MultiSubnetFailoverWithNonTcpProtocol = 49; internal const int MaxErrorValue = 50157; - internal const int LocalDBNoInstanceName = 51; - internal const int LocalDBNoInstallation = 52; - internal const int LocalDBInvalidConfig = 53; - internal const int LocalDBNoSqlUserInstanceDllPath = 54; - internal const int LocalDBInvalidSqlUserInstanceDllPath = 55; - internal const int LocalDBFailedToLoadDll = 56; - internal const int LocalDBBadRuntime = 57; internal const int SniIP6AddrStringBufferLength = 48; // from SNI layer internal static int SniMaxComposedSpnLength @@ -148,8 +140,6 @@ internal static bool NativeSetData(void* passedData, int passedSize) internal enum SniSpecialErrors : uint { - LocalDBErrorCode = SNINativeMethodWrapper.LocalDBErrorCode, - // multi-subnet-failover specific error codes MultiSubnetFailoverWithMoreThan64IPs = SNINativeMethodWrapper.MultiSubnetFailoverWithMoreThan64IPs, MultiSubnetFailoverWithInstanceSpecified = SNINativeMethodWrapper.MultiSubnetFailoverWithInstanceSpecified, diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/LocalDBAPI.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/LocalDBAPI.cs index e3b4ea2ef4..7ac16542f7 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/LocalDBAPI.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/LocalDBAPI.cs @@ -3,16 +3,7 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; -using System.Configuration; -using System.Diagnostics; -using System.Globalization; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; using System.Security; -using System.Text; -using System.Threading; -using Microsoft.Data.SqlClient; namespace Microsoft.Data { @@ -26,7 +17,6 @@ internal static class LocalDBAPI static bool _partialTrustFlagChecked = false; static bool _partialTrustAllowed = false; - // check if name is in format (localdb)\ and return instance name if it is internal static string GetLocalDbInstanceNameFromServerName(string serverName) { @@ -47,208 +37,10 @@ internal static string GetLocalDbInstanceNameFromServerName(string serverName) { return input.ToString(); } - } return null; } - internal static void ReleaseDLLHandles() - { - s_userInstanceDLLHandle = IntPtr.Zero; - s_localDBFormatMessage = null; - s_localDBCreateInstance = null; - } - - - - //This is copy of handle that SNI maintains, so we are responsible for freeing it - therefore there we are not using SafeHandle - static IntPtr s_userInstanceDLLHandle = IntPtr.Zero; - - static object s_dllLock = new object(); - - static IntPtr UserInstanceDLLHandle - { - get - { - if (s_userInstanceDLLHandle == IntPtr.Zero) - { - bool lockTaken = false; - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - Monitor.Enter(s_dllLock, ref lockTaken); - if (s_userInstanceDLLHandle == IntPtr.Zero) - { - SNINativeMethodWrapper.SNIQueryInfo(SNINativeMethodWrapper.QTypes.SNI_QUERY_LOCALDB_HMODULE, ref s_userInstanceDLLHandle); - if (s_userInstanceDLLHandle != IntPtr.Zero) - { - SqlClientEventSource.Log.TryTraceEvent(" LocalDB - handle obtained"); - } - else - { - SNINativeMethodWrapper.SNI_Error sniError = new SNINativeMethodWrapper.SNI_Error(); - SNINativeMethodWrapper.SNIGetLastError(out sniError); - throw CreateLocalDBException(errorMessage: StringsHelper.GetString("LocalDB_FailedGetDLLHandle"), sniError: (int)sniError.sniError); - } - } - } - finally - { - if (lockTaken) - Monitor.Exit(s_dllLock); - } - } - return s_userInstanceDLLHandle; - } - } - - [SuppressUnmanagedCodeSecurity] - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - private delegate int LocalDBCreateInstanceDelegate([MarshalAs(UnmanagedType.LPWStr)] string version, [MarshalAs(UnmanagedType.LPWStr)] string instance, UInt32 flags); - - static LocalDBCreateInstanceDelegate s_localDBCreateInstance = null; - - static LocalDBCreateInstanceDelegate LocalDBCreateInstance - { - get - { - if (s_localDBCreateInstance == null) - { - bool lockTaken = false; - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - Monitor.Enter(s_dllLock, ref lockTaken); - if (s_localDBCreateInstance == null) - { - IntPtr functionAddr = SafeNativeMethods.GetProcAddress(UserInstanceDLLHandle, "LocalDBCreateInstance"); - - if (functionAddr == IntPtr.Zero) - { - int hResult = Marshal.GetLastWin32Error(); - SqlClientEventSource.Log.TryTraceEvent(" GetProcAddress for LocalDBCreateInstance error 0x{0}", hResult); - throw CreateLocalDBException(errorMessage: StringsHelper.GetString("LocalDB_MethodNotFound")); - } - s_localDBCreateInstance = (LocalDBCreateInstanceDelegate)Marshal.GetDelegateForFunctionPointer(functionAddr, typeof(LocalDBCreateInstanceDelegate)); - } - } - finally - { - if (lockTaken) - Monitor.Exit(s_dllLock); - } - } - return s_localDBCreateInstance; - } - } - - - [SuppressUnmanagedCodeSecurity] - [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Unicode)] - private delegate int LocalDBFormatMessageDelegate(int hrLocalDB, UInt32 dwFlags, UInt32 dwLanguageId, StringBuilder buffer, ref UInt32 buflen); - - static LocalDBFormatMessageDelegate s_localDBFormatMessage = null; - - static LocalDBFormatMessageDelegate LocalDBFormatMessage - { - get - { - if (s_localDBFormatMessage == null) - { - bool lockTaken = false; - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - Monitor.Enter(s_dllLock, ref lockTaken); - if (s_localDBFormatMessage == null) - { - IntPtr functionAddr = SafeNativeMethods.GetProcAddress(UserInstanceDLLHandle, "LocalDBFormatMessage"); - - if (functionAddr == IntPtr.Zero) - { - // SNI checks for LocalDBFormatMessage during DLL loading, so it is practically impossible to get this error. - int hResult = Marshal.GetLastWin32Error(); - SqlClientEventSource.Log.TryTraceEvent(" GetProcAddress for LocalDBFormatMessage error 0x{0}", hResult); - throw CreateLocalDBException(errorMessage: StringsHelper.GetString("LocalDB_MethodNotFound")); - } - s_localDBFormatMessage = (LocalDBFormatMessageDelegate)Marshal.GetDelegateForFunctionPointer(functionAddr, typeof(LocalDBFormatMessageDelegate)); - } - } - finally - { - if (lockTaken) - Monitor.Exit(s_dllLock); - } - } - return s_localDBFormatMessage; - } - } - - const UInt32 const_LOCALDB_TRUNCATE_ERR_MESSAGE = 1;// flag for LocalDBFormatMessage that indicates that message can be truncated if it does not fit in the buffer - const int const_ErrorMessageBufferSize = 1024; // Buffer size for Local DB error message, according to Serverless team, 1K will be enough for all messages - - - internal static string GetLocalDBMessage(int hrCode) - { - Debug.Assert(hrCode < 0, "HRCode does not indicate error"); - try - { - StringBuilder buffer = new StringBuilder((int)const_ErrorMessageBufferSize); - UInt32 len = (UInt32)buffer.Capacity; - - - // First try for current culture - int hResult = LocalDBFormatMessage(hrLocalDB: hrCode, dwFlags: const_LOCALDB_TRUNCATE_ERR_MESSAGE, dwLanguageId: (UInt32)CultureInfo.CurrentCulture.LCID, - buffer: buffer, buflen: ref len); - if (hResult >= 0) - return buffer.ToString(); - else - { - // Message is not available for current culture, try default - buffer = new StringBuilder((int)const_ErrorMessageBufferSize); - len = (UInt32)buffer.Capacity; - hResult = LocalDBFormatMessage(hrLocalDB: hrCode, dwFlags: const_LOCALDB_TRUNCATE_ERR_MESSAGE, dwLanguageId: 0 /* thread locale with fallback to English */, - buffer: buffer, buflen: ref len); - if (hResult >= 0) - return buffer.ToString(); - else - return string.Format(CultureInfo.CurrentCulture, "{0} (0x{1:X}).", StringsHelper.GetString("LocalDB_UnobtainableMessage"), hResult); - } - } - catch (SqlException exc) - { - return string.Format(CultureInfo.CurrentCulture, "{0} ({1}).", StringsHelper.GetString("LocalDB_UnobtainableMessage"), exc.Message); - } - } - - - static SqlException CreateLocalDBException(string errorMessage, string instance = null, int localDbError = 0, int sniError = 0) - { - Debug.Assert((localDbError == 0) || (sniError == 0), "LocalDB error and SNI error cannot be specified simultaneously"); - Debug.Assert(!string.IsNullOrEmpty(errorMessage), "Error message should not be null or empty"); - SqlErrorCollection collection = new SqlErrorCollection(); - - int errorCode = (localDbError == 0) ? sniError : localDbError; - - if (sniError != 0) - { - string sniErrorMessage = SQL.GetSNIErrorMessage(sniError); - errorMessage = String.Format((IFormatProvider)null, "{0} (error: {1} - {2})", - errorMessage, sniError, sniErrorMessage); - } - - collection.Add(new SqlError(errorCode, 0, TdsEnums.FATAL_ERROR_CLASS, instance, errorMessage, null, 0)); - - if (localDbError != 0) - collection.Add(new SqlError(errorCode, 0, TdsEnums.FATAL_ERROR_CLASS, instance, GetLocalDBMessage(localDbError), null, 0)); - - SqlException exc = SqlException.CreateException(collection, null); - - exc._doNotReconnect = true; - - return exc; - } - private class InstanceInfo { internal InstanceInfo(string version) @@ -261,9 +53,6 @@ internal InstanceInfo(string version) internal bool created; } - static object s_configLock = new object(); - static Dictionary s_configurableInstances = null; - internal static void DemandLocalDBPermissions() { if (!_partialTrustAllowed) @@ -293,73 +82,5 @@ internal static void AssertLocalDBPermissions() { _partialTrustAllowed = true; } - - - internal static void CreateLocalDBInstance(string instance) - { - DemandLocalDBPermissions(); - if (s_configurableInstances == null) - { - // load list of instances from configuration, mark them as not created - bool lockTaken = false; - RuntimeHelpers.PrepareConstrainedRegions(); - try - { - Monitor.Enter(s_configLock, ref lockTaken); - if (s_configurableInstances == null) - { - Dictionary tempConfigurableInstances = new Dictionary(StringComparer.OrdinalIgnoreCase); - object section = ConfigurationManager.GetSection("system.data.localdb"); - if (section != null) // if no section just skip creation - { - // validate section type - LocalDBConfigurationSection configSection = section as LocalDBConfigurationSection; - if (configSection == null) - throw CreateLocalDBException(errorMessage: StringsHelper.GetString("LocalDB_BadConfigSectionType")); - foreach (LocalDBInstanceElement confElement in configSection.LocalDbInstances) - { - Debug.Assert(confElement.Name != null && confElement.Version != null, "Both name and version should not be null"); - tempConfigurableInstances.Add(confElement.Name.Trim(), new InstanceInfo(confElement.Version.Trim())); - } - } - else - { - SqlClientEventSource.Log.TryTraceEvent(" No system.data.localdb section found in configuration"); - } - s_configurableInstances = tempConfigurableInstances; - } - } - finally - { - if (lockTaken) - Monitor.Exit(s_configLock); - } - } - - InstanceInfo instanceInfo = null; - - if (!s_configurableInstances.TryGetValue(instance, out instanceInfo)) - return; // instance name was not in the config - - if (instanceInfo.created) - return; // instance has already been created - - Debug.Assert(!instance.Contains("\0"), "Instance name should contain embedded nulls"); - - if (instanceInfo.version.Contains("\0")) - throw CreateLocalDBException(errorMessage: StringsHelper.GetString("LocalDB_InvalidVersion"), instance: instance); - - // LocalDBCreateInstance is thread- and cross-process safe method, it is OK to call from two threads simultaneously - int hr = LocalDBCreateInstance(instanceInfo.version, instance, flags: 0); - SqlClientEventSource.Log.TryTraceEvent(" Starting creation of instance {0} version {1}", instance, instanceInfo.version); - - if (hr < 0) - { - throw CreateLocalDBException(errorMessage: StringsHelper.GetString("LocalDB_CreateFailed"), instance: instance, localDbError: hr); - } - - SqlClientEventSource.Log.TryTraceEvent(" Finished creation of instance {0}", instance); - instanceInfo.created = true; // mark instance as created - } // CreateLocalDbInstance } } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/LocalDBConfig.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/LocalDBConfig.cs deleted file mode 100644 index 4c830520d9..0000000000 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/LocalDBConfig.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Microsoft.Data -{ - using System; - using System.Collections; - using System.Configuration; - - internal sealed class LocalDBInstanceElement : ConfigurationElement - { - [ConfigurationProperty("name", IsRequired = true)] - public string Name - { - get - { - return this["name"] as string; - } - } - - [ConfigurationProperty("version", IsRequired = true)] - public string Version - { - get - { - return this["version"] as string; - } - } - } - - internal sealed class LocalDBInstancesCollection : ConfigurationElementCollection - { - - private class TrimOrdinalIgnoreCaseStringComparer : IComparer - { - public int Compare(object x, object y) - { - string xStr = x as string; - if (xStr != null) - x = xStr.Trim(); - - string yStr = y as string; - if (yStr != null) - y = yStr.Trim(); - - return StringComparer.OrdinalIgnoreCase.Compare(x, y); - } - } - - static readonly TrimOrdinalIgnoreCaseStringComparer s_comparer = new TrimOrdinalIgnoreCaseStringComparer(); - - internal LocalDBInstancesCollection() - : base(s_comparer) - { - } - - protected override ConfigurationElement CreateNewElement() - { - return new LocalDBInstanceElement(); - } - - protected override object GetElementKey(ConfigurationElement element) - { - return ((LocalDBInstanceElement)element).Name; - } - - } - - internal sealed class LocalDBConfigurationSection : ConfigurationSection - { - [ConfigurationProperty("localdbinstances", IsRequired = true)] - public LocalDBInstancesCollection LocalDbInstances - { - get - { - return (LocalDBInstancesCollection)this["localdbinstances"] ?? new LocalDBInstancesCollection(); - } - } - } -} diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs index aa6a670021..068b37dc71 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlInternalConnectionTds.cs @@ -2300,7 +2300,7 @@ private void AttemptOneLogin(ServerInfo serverInfo, string newPassword, SecureSt _parser.Connect(serverInfo, this, - timeout.LegacyTimerExpire, + timeout, ConnectionOptions, withFailover, isFirstTransparentAttempt, diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 21abbab757..e6759bf7f2 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -10,6 +10,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Net; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; @@ -17,8 +18,8 @@ using System.Threading; using System.Threading.Tasks; using System.Xml; -using System.Net; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; using Microsoft.Data.Sql; using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.Server; @@ -494,7 +495,7 @@ internal void ProcessPendingAck(TdsParserStateObject stateObj) internal void Connect(ServerInfo serverInfo, SqlInternalConnectionTds connHandler, - long timerExpire, + TimeoutTimer timerExpire, SqlConnectionString connectionOptions, bool withFailover, bool isFirstTransparentAttempt, @@ -536,7 +537,17 @@ internal void Connect(ServerInfo serverInfo, //Create LocalDB instance if necessary if (connHandler.ConnectionOptions.LocalDBInstance != null) { - LocalDBAPI.CreateLocalDBInstance(connHandler.ConnectionOptions.LocalDBInstance); + //do not get np result if data source is already np + string[] splitByColon = serverInfo.UserServerName.Split(':'); + if (!splitByColon[0].Trim().Equals("np", StringComparison.CurrentCultureIgnoreCase)) + { + string localDBDataSource = LocalDB.GetLocalDBDataSource(serverInfo.UserServerName, timerExpire); + string serverName = localDBDataSource ?? serverInfo.ExtendedServerName; + //re-writing serverinfo + ServerInfo original = serverInfo; + serverInfo = new ServerInfo(connectionOptions, serverName, original.ServerSPN); + serverInfo.SetDerivedNames(original.UserProtocol, serverName); + } if (encrypt == SqlConnectionEncryptOption.Mandatory) { // Encryption is not supported on SQL Local DB - disable it for current session. @@ -679,7 +690,7 @@ internal void Connect(ServerInfo serverInfo, } _state = TdsParserState.OpenNotLoggedIn; _physicalStateObj.SniContext = SniContext.Snix_PreLoginBeforeSuccessfulWrite; // SQL BU DT 376766 - _physicalStateObj.TimeoutTime = timerExpire; + _physicalStateObj.TimeoutTime = timerExpire.LegacyTimerExpire; bool marsCapable = false; @@ -1934,13 +1945,6 @@ internal SqlError ProcessSNIError(TdsParserStateObject stateObj) // SNI error. Replace the entire message // errorMessage = SQL.GetSNIErrorMessage((int)sniError.sniError); - - // If its a LocalDB error, then nativeError actually contains a LocalDB-specific error code, not a win32 error code - if (sniError.sniError == (int)SNINativeMethodWrapper.SniSpecialErrors.LocalDBErrorCode) - { - errorMessage += LocalDBAPI.GetLocalDBMessage((int)sniError.nativeError); - win32ErrorCode = 0; - } } errorMessage = string.Format("{0} (provider: {1}, error: {2} - {3})", sqlContextInfo, providerName, (int)sniError.sniError, errorMessage); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index 5d5ade91f4..4967ad62a8 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -12,6 +12,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Data.Common; +using Microsoft.Data.ProviderBase; namespace Microsoft.Data.SqlClient { @@ -279,7 +280,7 @@ private SNINativeMethodWrapper.ConsumerInfo CreateConsumerInfo(bool async) internal void CreatePhysicalSNIHandle( string serverName, - long timerExpire, + TimeoutTimer timerExpire, out byte[] instanceName, byte[] spnBuffer, bool flushCache, @@ -295,13 +296,13 @@ internal void CreatePhysicalSNIHandle( // Translate to SNI timeout values (Int32 milliseconds) long timeout; - if (long.MaxValue == timerExpire) + if (long.MaxValue == timerExpire.LegacyTimerExpire) { timeout = int.MaxValue; } else { - timeout = ADP.TimerRemainingMilliseconds(timerExpire); + timeout = ADP.TimerRemainingMilliseconds(timerExpire.LegacyTimerExpire); if (timeout > int.MaxValue) { timeout = int.MaxValue; diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs index 5fd4a54382..4579a24131 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs @@ -6972,6 +6972,15 @@ internal static string LocalDB_MethodNotFound { } } + /// + /// Looks up a localized string similar to SQLUserInstanceDLL is not supported on current plaform. Install SqlLocalDB as an alternate option to connect to LocalDB.. + /// + internal static string LocalDB_SQLUserInstanceDLL { + get { + return ResourceManager.GetString("LocalDB_SQLUserInstanceDLL", resourceCulture); + } + } + /// /// Looks up a localized string similar to Cannot obtain Local Database Runtime error message. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx index 2b2374e490..08096e95c5 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx @@ -4644,4 +4644,7 @@ Cannot set the AccessTokenCallback property if 'Authentication=Active Directory Default' has been specified in the connection string. + + SQLUserInstanceDLL is not supported on current plaform. Install SqlLocalDB as an alternate option to connect to LocalDB. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.Unix.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.Unix.cs index 8b84feecfc..43e5ff0aed 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.Unix.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.Unix.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; + namespace Microsoft.Data.Common { /// @@ -20,5 +22,8 @@ internal static object LocalMachineRegistryValue(string subkey, string queryvalu // No registry in non-Windows environments return null; } + + internal static PlatformNotSupportedException LocalDBNotSUpportedException() => + new PlatformNotSupportedException(Strings.LocalDBNotSupported); // LocalDB is not available for Unix and hence it cannot be supported. } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/TimeoutTimer.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/TimeoutTimer.cs index 9948b223d1..c8db6f97e1 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/TimeoutTimer.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/ProviderBase/TimeoutTimer.cs @@ -2,9 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.Data.Common; using System; using System.Diagnostics; +using Microsoft.Data.Common; namespace Microsoft.Data.ProviderBase { @@ -180,6 +180,42 @@ internal long MillisecondsRemaining return milliseconds; } } + + // Returns milliseconds remaining trimmed to zero for none remaining + internal int MillisecondsRemainingInt + { + get + { + //------------------- + // Method Body + int milliseconds; + if (_isInfiniteTimeout) + { + milliseconds = int.MaxValue; + } + else + { + long longMilliseconds = ADP.TimerRemainingMilliseconds(_timerExpire); + if (0 > longMilliseconds) + { + milliseconds = 0; + } + else if (longMilliseconds > int.MaxValue) + { + milliseconds = int.MaxValue; + } + else + { + milliseconds = (int)longMilliseconds; + } + } + + //-------------------- + // Postconditions + Debug.Assert(0 <= milliseconds); + + return milliseconds; + } + } } } - diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/DataSource.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/DataSource.cs new file mode 100644 index 0000000000..4bcd67c348 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/DataSource.cs @@ -0,0 +1,411 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Text; + +namespace Microsoft.Data.SqlClient +{ + internal class DataSource + { + private const char CommaSeparator = ','; + private const char SemiColon = ':'; + private const char BackSlashCharacter = '\\'; + + private const string DefaultHostName = "localhost"; + private const string DefaultSqlServerInstanceName = "mssqlserver"; + private const string PipeBeginning = @"\\"; + private const string Slash = @"/"; + private const string PipeToken = "pipe"; + private const string LocalDbHost = "(localdb)"; + private const string LocalDbHost_NP = @"np:\\.\pipe\LOCALDB#"; + private const string NamedPipeInstanceNameHeader = "mssql$"; + private const string DefaultPipeName = "sql\\query"; + + internal const int LocalDBNoInstanceName = 51; + internal const int LocalDBNoInstallation = 52; + internal const int LocalDBInvalidConfig = 53; + internal const int LocalDBNoSqlUserInstanceDllPath = 54; + internal const int LocalDBInvalidSqlUserInstanceDllPath = 55; + internal const int LocalDBFailedToLoadDll = 56; + internal const int LocalDBBadRuntime = 57; + internal const int InvalidConnStringError = 25; + + private enum ProviderEnum + { + HTTP_PROV, + NP_PROV, + SESSION_PROV, + SIGN_PROV, + SM_PROV, + SMUX_PROV, + SSL_PROV, + TCP_PROV, + VIA_PROV, + CTAIP_PROV, + MAX_PROVS, + INVALID_PROV, + } + internal enum Protocol { TCP, NP, None, Admin }; + + internal Protocol _connectionProtocol = Protocol.None; + + /// + /// Provides the HostName of the server to connect to for TCP protocol. + /// This information is also used for finding the SPN of SqlServer + /// + internal string ServerName { get; private set; } + + /// + /// Provides the port on which the TCP connection should be made if one was specified in Data Source + /// + internal int Port { get; private set; } = -1; + + /// + /// Provides the inferred Instance Name from Server Data Source + /// + internal string InstanceName { get; private set; } + + /// + /// Provides the pipe name in case of Named Pipes + /// + internal string PipeName { get; private set; } + + /// + /// Provides the HostName to connect to in case of Named pipes Data Source + /// + internal string PipeHostName { get; private set; } + + private readonly string _workingDataSource; + private readonly string _dataSourceAfterTrimmingProtocol; + + internal bool IsBadDataSource { get; private set; } = false; + + internal bool IsSsrpRequired { get; private set; } = false; + + private DataSource(string dataSource) + { + // Remove all whitespaces from the datasource and all operations will happen on lower case. + _workingDataSource = dataSource.Trim().ToLowerInvariant(); + + int firstIndexOfColon = _workingDataSource.IndexOf(SemiColon); + + PopulateProtocol(); + + _dataSourceAfterTrimmingProtocol = (firstIndexOfColon > -1) && _connectionProtocol != Protocol.None + ? _workingDataSource.Substring(firstIndexOfColon + 1).Trim() : _workingDataSource; + + if (_dataSourceAfterTrimmingProtocol.Contains(Slash)) // Pipe paths only allow back slashes + { + if (_connectionProtocol == Protocol.None) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.INVALID_PROV, ServerName), ""); + } + else if (_connectionProtocol == Protocol.NP) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.NP_PROV, ServerName), ""); + } + else if (_connectionProtocol == Protocol.TCP) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.TCP_PROV, ServerName), ""); + } + } + } + + private void PopulateProtocol() + { + string[] splitByColon = _workingDataSource.Split(SemiColon); + + if (splitByColon.Length <= 1) + { + _connectionProtocol = Protocol.None; + } + else + { + // We trim before switching because " tcp : server , 1433 " is a valid data source + switch (splitByColon[0].Trim()) + { + case TdsEnums.TCP: + _connectionProtocol = Protocol.TCP; + break; + case TdsEnums.NP: + _connectionProtocol = Protocol.NP; + break; + case TdsEnums.ADMIN: + _connectionProtocol = Protocol.Admin; + break; + default: + // None of the supported protocols were found. This may be a IPv6 address + _connectionProtocol = Protocol.None; + break; + } + } + } + + // LocalDbInstance name always starts with (localdb) + // possible scenarios: + // (localdb)\ + // or (localdb)\. which goes to default localdb + // or (localdb)\.\ + internal static string GetLocalDBInstance(string dataSource, out bool error) + { + string instanceName = null; + // ReadOnlySpan is not supported in netstandard 2.0, but installing System.Memory solves the issue + ReadOnlySpan input = dataSource.AsSpan().TrimStart(); + error = false; + // NetStandard 2.0 does not support passing a string to ReadOnlySpan + if (input.StartsWith(LocalDbHost.AsSpan().Trim(), StringComparison.InvariantCultureIgnoreCase)) + { + // When netcoreapp support for netcoreapp2.1 is dropped these slice calls could be converted to System.Range\System.Index + // Such ad input = input[1..]; + input = input.Slice(LocalDbHost.Length); + if (!input.IsEmpty && input[0] == BackSlashCharacter) + { + input = input.Slice(1); + } + if (!input.IsEmpty) + { + instanceName = input.Trim().ToString(); + } + else + { + error = true; + SqlErrorCollection errors = new SqlErrorCollection + { + new SqlError(InvalidConnStringError, 0, TdsEnums.FATAL_ERROR_CLASS, dataSource, string.Format("(provider: {0}, error: {1} - {2})", + ProviderEnum.INVALID_PROV, LocalDBNoInstanceName, Strings.SNI_ERROR_51), null, 0) + }; + throw SqlException.CreateException(errors, ""); + } + } + else if (input.StartsWith(LocalDbHost_NP.AsSpan().Trim(), StringComparison.InvariantCultureIgnoreCase)) + { + instanceName = input.Trim().ToString(); + } + + return instanceName; + } + private static SqlErrorCollection LocalDBErrorFormat(ProviderEnum providerName, string ServerName) + { + return new SqlErrorCollection + { + new SqlError(InvalidConnStringError, 0, TdsEnums.MIN_ERROR_CLASS, ServerName, string.Format("(provider: {0}, error: {1} - {2})", + providerName, InvalidConnStringError, Strings.SNI_ERROR_25), null, 0) + }; + } + + internal static DataSource ParseServerName(string dataSource) + { + DataSource details = new DataSource(dataSource); + + if (details.IsBadDataSource) + { + return null; + } + + if (details.InferNamedPipesInformation()) + { + return details; + } + + if (details.IsBadDataSource) + { + return null; + } + + if (details.InferConnectionDetails()) + { + return details; + } + + return null; + } + + private void InferLocalServerName() + { + // If Server name is empty or localhost, then use "localhost" + if (string.IsNullOrEmpty(ServerName) || IsLocalHost(ServerName) || + (Environment.MachineName.Equals(ServerName, StringComparison.CurrentCultureIgnoreCase) && + _connectionProtocol == Protocol.Admin)) + { + // For DAC use "localhost" instead of the server name. + ServerName = DefaultHostName; + } + } + + private bool InferConnectionDetails() + { + string[] tokensByCommaAndSlash = _dataSourceAfterTrimmingProtocol.Split(BackSlashCharacter, CommaSeparator); + ServerName = tokensByCommaAndSlash[0].Trim(); + + int commaIndex = _dataSourceAfterTrimmingProtocol.IndexOf(CommaSeparator); + + int backSlashIndex = _dataSourceAfterTrimmingProtocol.IndexOf(BackSlashCharacter); + + // Check the parameters. The parameters are Comma separated in the Data Source. The parameter we really care about is the port + // If Comma exists, the try to get the port number + if (commaIndex > -1) + { + string parameter = backSlashIndex > -1 + ? ((commaIndex > backSlashIndex) ? tokensByCommaAndSlash[2].Trim() : tokensByCommaAndSlash[1].Trim()) + : tokensByCommaAndSlash[1].Trim(); + + // Bad Data Source like "server, " + if (string.IsNullOrEmpty(parameter)) + { + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.INVALID_PROV, ServerName), ""); + } + + // For Tcp and Only Tcp are parameters allowed. + if (_connectionProtocol == Protocol.None) + { + _connectionProtocol = Protocol.TCP; + } + else if (_connectionProtocol != Protocol.TCP) + { + IsBadDataSource = true; + // Parameter has been specified for non-TCP protocol. This is not allowed. + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.INVALID_PROV, ServerName), ""); + } + + int port; + if (!int.TryParse(parameter, out port)) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.TCP_PROV, ServerName), ""); + } + + // If the user explicitly specified a invalid port in the connection string. + if (port < 1) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.TCP_PROV, ServerName), ""); + } + + Port = port; + } + // Instance Name Handling. Only if we found a '\' and we did not find a port in the Data Source + else if (backSlashIndex > -1) + { + // This means that there will not be any part separated by comma. + InstanceName = tokensByCommaAndSlash[1].Trim(); + + if (string.IsNullOrWhiteSpace(InstanceName)) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.INVALID_PROV, ServerName), ""); + } + + if (DefaultSqlServerInstanceName.Equals(InstanceName)) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.INVALID_PROV, ServerName), ""); + } + + IsSsrpRequired = true; + } + + InferLocalServerName(); + + return true; + } + + private bool InferNamedPipesInformation() + { + // If we have a datasource beginning with a pipe or we have already determined that the protocol is Named Pipe + if (_dataSourceAfterTrimmingProtocol.StartsWith(PipeBeginning) || _connectionProtocol == Protocol.NP) + { + // If the data source is "np:servername" + if (!_dataSourceAfterTrimmingProtocol.Contains(PipeBeginning)) + { + PipeHostName = ServerName = _dataSourceAfterTrimmingProtocol; + InferLocalServerName(); + PipeName = DefaultPipeName; + return true; + } + + try + { + string[] tokensByBackSlash = _dataSourceAfterTrimmingProtocol.Split(BackSlashCharacter); + + // The datasource is of the format \\host\pipe\sql\query [0]\[1]\[2]\[3]\[4]\[5] + // It would at least have 6 parts. + // Another valid Sql named pipe for an named instance is \\.\pipe\MSSQL$MYINSTANCE\sql\query + if (tokensByBackSlash.Length < 6) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.NP_PROV, ServerName), ""); + } + + string host = tokensByBackSlash[2]; + + if (string.IsNullOrEmpty(host)) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.NP_PROV, ServerName), ""); + } + + //Check if the "pipe" keyword is the first part of path + if (!PipeToken.Equals(tokensByBackSlash[3])) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.NP_PROV, ServerName), ""); + } + + if (tokensByBackSlash[4].StartsWith(NamedPipeInstanceNameHeader)) + { + InstanceName = tokensByBackSlash[4].Substring(NamedPipeInstanceNameHeader.Length); + } + + StringBuilder pipeNameBuilder = new StringBuilder(); + + for (int i = 4; i < tokensByBackSlash.Length - 1; i++) + { + pipeNameBuilder.Append(tokensByBackSlash[i]); + pipeNameBuilder.Append(Path.DirectorySeparatorChar); + } + // Append the last part without a "/" + pipeNameBuilder.Append(tokensByBackSlash[tokensByBackSlash.Length - 1]); + PipeName = pipeNameBuilder.ToString(); + + if (string.IsNullOrWhiteSpace(InstanceName) && !DefaultPipeName.Equals(PipeName)) + { + InstanceName = PipeToken + PipeName; + } + + ServerName = IsLocalHost(host) ? Environment.MachineName : host; + // Pipe hostname is the hostname after leading \\ which should be passed down as is to open Named Pipe. + // For Named Pipes the ServerName makes sense for SPN creation only. + PipeHostName = host; + } + catch (UriFormatException) + { + IsBadDataSource = true; + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.NP_PROV, ServerName), ""); + } + + // DataSource is something like "\\pipename" + if (_connectionProtocol == Protocol.None) + { + _connectionProtocol = Protocol.NP; + } + else if (_connectionProtocol != Protocol.NP) + { + IsBadDataSource = true; + // In case the path began with a "\\" and protocol was not Named Pipes + throw SqlException.CreateException(LocalDBErrorFormat(ProviderEnum.NP_PROV, ServerName), ""); + } + return true; + } + return false; + } + + private static bool IsLocalHost(string serverName) + => ".".Equals(serverName) || "(local)".Equals(serverName) || "localhost".Equals(serverName); + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalDB.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalDB.Windows.cs new file mode 100644 index 0000000000..b1437f4343 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/LocalDB.Windows.cs @@ -0,0 +1,404 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Data.ProviderBase; +using Microsoft.Win32; +using Microsoft.Win32.SafeHandles; + +namespace Microsoft.Data.SqlClient +{ + internal sealed class LocalDB + { + private static readonly LocalDB Instance = new LocalDB(); + private const string ClassName = nameof(LocalDB); + //HKEY_LOCAL_MACHINE + private const string LocalDBInstalledVersionRegistryKey = "SOFTWARE\\Microsoft\\Microsoft SQL Server Local DB\\Installed Versions\\"; + + // Set a non-infinite timeout on the regex to ensure we never hang. 10 seconds should + // be more than enough as a fail-safe for the small amount of text that is going to be processed. Since + // we can't set the timeout on each Match() call, this prioritizes perf (RegexOptions.Compiled) over + // the small possiblity that we might go a little over a connect timeout during extremely high load. + private static readonly Regex regex = new Regex("(np:.+)\r", RegexOptions.Compiled | RegexOptions.CultureInvariant, TimeSpan.FromSeconds(10)); + + private const string InstanceAPIPathValueName = "InstanceAPIPath"; + private const string ProcLocalDBStartInstance = "LocalDBStartInstance"; + private const int MAX_LOCAL_DB_CONNECTION_STRING_SIZE = 260; + private IntPtr _startInstanceHandle = IntPtr.Zero; + private static Lazy s_sqlLocalDBExe = new Lazy(() => GetPathToSqlLocalDB()); + + // Local Db api doc https://msdn.microsoft.com/en-us/library/hh217143.aspx + // HRESULT LocalDBStartInstance( [Input ] PCWSTR pInstanceName, [Input ] DWORD dwFlags,[Output] LPWSTR wszSqlConnection,[Input/Output] LPDWORD lpcchSqlConnection); + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + internal delegate int LocalDBStartInstance( + [In] [MarshalAs(UnmanagedType.LPWStr)] string localDBInstanceName, + [In] int flags, + [Out] [MarshalAs(UnmanagedType.LPWStr)] StringBuilder sqlConnectionDataSource, + [In, Out]ref int bufferLength); + + private LocalDBStartInstance localDBStartInstanceFunc = null; + + private volatile SafeLibraryHandle _sqlUserInstanceLibraryHandle; + + private LocalDB() { } + + internal static string GetLocalDBConnectionString(string localDbInstance, TimeoutTimer timeout) + { + try + { + return Instance.LoadUserInstanceDll() ? Instance.GetConnectionString(localDbInstance) : null; + } + catch(Exception ex) + { + SqlClientEventSource.Log.TryTraceEvent(ClassName, EventType.ERR,ex?.Message); + SqlClientEventSource.Log.TryTraceEvent(ClassName, EventType.INFO, "Falling back to use SqlLocalDB.exe."); + + // The old logic to load the SqlUserInstance.dll did not quite work possibly because + // of an archiecture mismatch (e.g. we are running in an ARM64 process and SqlLocalDB.exe + // installed on the machine is an AMD64 process). + // We try to figure out what we need by asking SqlLocalDB.exe (out of proc). + + if (!TryGetLocalDBConnectionStringUsingSqlLocalDBExe(localDbInstance, timeout, out string connString, out string error)) + { + SqlClientEventSource.Log.TryTraceEvent(ClassName, EventType.ERR, "Unable to to use SqlLocalDB.exe to get the ConnectionString."); + //errors from localdb.exe + if (error != null) + throw new Exception(error, ex); + else + throw new Exception(StringsHelper.LocalDB_SQLUserInstanceDLL, ex); + } + return connString; + } + } + + private static string GetPathToSqlLocalDB() + { + RegistryKey mssqlRegKey = null; + + try + { + mssqlRegKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Microsoft SQL Server"); + if (mssqlRegKey != null) + { + string[] alphaNumnericKeys = mssqlRegKey.GetSubKeyNames(); + List intKeys =new List(); + for(int i =0;i=0;item--) + { + using (RegistryKey sk = mssqlRegKey.OpenSubKey($@"{intKeys[item]}\Tools\\ClientSetup", writable: false)) + { + if (sk.GetValue("Path") is string value) + { + string path = Path.Combine(value, "SqlLocalDB.exe"); + if (File.Exists(path)) + { + return path; + } + } + } + } + } + } + catch + { + SqlClientEventSource.Log.TryTraceEvent(ClassName, EventType.ERR, "No File path exist for SqlLocalDB.exe under Registry key."); + } + finally + { + mssqlRegKey?.Close(); + } + + return null; + } + + private string GetConnectionString(string localDbInstance) + { + StringBuilder localDBConnectionString = new StringBuilder(MAX_LOCAL_DB_CONNECTION_STRING_SIZE + 1); + int sizeOfbuffer = localDBConnectionString.Capacity; + localDBStartInstanceFunc(localDbInstance, 0, localDBConnectionString, ref sizeOfbuffer); + return localDBConnectionString.ToString(); + } + private static bool TryGetLocalDBConnectionStringUsingSqlLocalDBExe(string localDbInstance, TimeoutTimer timeout, out string connString, out string error) + { + connString = null; + error = null; + try + { + // Make sure the instance is running first. If it is, this call won't do any harm, aside from + // wasting some time. + var psi = new ProcessStartInfo(s_sqlLocalDBExe.Value, $"s \"{localDbInstance}\"") + { + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + + var proc = Process.Start(psi); + + if (proc.WaitForExit(milliseconds: timeout.MillisecondsRemainingInt)) + { + psi = new ProcessStartInfo(s_sqlLocalDBExe.Value, $"i \"{localDbInstance}\"") + { + UseShellExecute = false, + RedirectStandardOutput = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + + proc = Process.Start(psi); + + if (proc.WaitForExit(milliseconds: timeout.MillisecondsRemainingInt)) + { + var alllines = proc.StandardOutput.ReadToEnd(); + + SqlClientEventSource.Log.TryTraceEvent(ClassName, EventType.INFO, $"Called: {s_sqlLocalDBExe.Value} \"{localDbInstance}\""); + + Match match = regex.Match(alllines); + if (match.Success) + { + connString = match.Value.Trim(); + return true; + } + else + { + error = alllines; + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.INFO, "No match found for named pipe SqlLocalDB.exe process stdout."); + } + } + } + } + catch(RegexMatchTimeoutException ex) + { + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.ERR, "Unable to retrieve named pipe SqlLocalDB.exe process stdout, it took longer than the maximum time allowed."+ex?.Message); + } + catch + { + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.ERR, "Unable to start LocalDB.exe process."); + } + + return false; + } + + internal enum LocalDBErrorState + { + NO_INSTALLATION, INVALID_CONFIG, NO_SQLUSERINSTANCEDLL_PATH, INVALID_SQLUSERINSTANCEDLL_PATH, NONE + } + + /// + /// Loads the User Instance dll. + /// + private bool LoadUserInstanceDll() + { + // Check in a non thread-safe way if the handle is already set for performance. + if (_sqlUserInstanceLibraryHandle != null) + { + return true; + } + + lock (this) + { + if (_sqlUserInstanceLibraryHandle != null) + { + return true; + } + //Get UserInstance Dll path + LocalDBErrorState registryQueryErrorState; + + // Get the LocalDB instance dll path from the registry + string dllPath = GetUserInstanceDllPath(out registryQueryErrorState); + + // If there was no DLL path found, then there is an error. + if (dllPath == null) + { + SqlClientEventSource.Log.TraceEvent(nameof(LocalDB), EventType.ERR, "User instance DLL path is null."); + throw new Exception(MapLocalDBErrorStateToErrorMessage(registryQueryErrorState)); + } + + // In case the registry had an empty path for dll + if (string.IsNullOrWhiteSpace(dllPath)) + { + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.ERR, "User instance DLL path is invalid. DLL path = {0}", dllPath); + throw new Exception(Strings.SNI_ERROR_55); + } + + // Load the dll + SafeLibraryHandle libraryHandle = Interop.Kernel32.LoadLibraryExW(dllPath.Trim(), IntPtr.Zero, 0); + + if (libraryHandle.IsInvalid) + { + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.ERR, "Library Handle is invalid. Could not load the dll."); + libraryHandle.Dispose(); + throw new Exception(Strings.SNI_ERROR_56); + } + + // Load the procs from the DLLs + _startInstanceHandle = Interop.Kernel32.GetProcAddress(libraryHandle, ProcLocalDBStartInstance); + + if (_startInstanceHandle == IntPtr.Zero) + { + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.ERR, "Was not able to load the PROC from DLL. Bad Runtime."); + libraryHandle.Dispose(); + throw new Exception(Strings.SNI_ERROR_57); + } + + // Set the delegate the invoke. + localDBStartInstanceFunc = (LocalDBStartInstance)Marshal.GetDelegateForFunctionPointer(_startInstanceHandle, typeof(LocalDBStartInstance)); + + if (localDBStartInstanceFunc == null) + { + libraryHandle.Dispose(); + _startInstanceHandle = IntPtr.Zero; + throw new Exception(Strings.SNI_ERROR_57); + } + + _sqlUserInstanceLibraryHandle = libraryHandle; + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.INFO, "User Instance DLL was loaded successfully."); + return true; + } + } + + /// + /// Gets the Local db Named pipe data source if the input is a localDB server. + /// + /// The data source + /// + /// + internal static string GetLocalDBDataSource(string fullServerName, TimeoutTimer timeout) + { + string localDBConnectionString = null; + string localDBInstance = DataSource.GetLocalDBInstance(fullServerName, out bool isBadLocalDBDataSource); + + if (isBadLocalDBDataSource) + { + return null; + } + + else if (!string.IsNullOrEmpty(localDBInstance)) + { + // We have successfully received a localDBInstance which is valid. + Debug.Assert(!string.IsNullOrWhiteSpace(localDBInstance), "Local DB Instance name cannot be empty."); + localDBConnectionString = LocalDB.GetLocalDBConnectionString(localDBInstance, timeout); + + if (fullServerName == null) + { + // The Last error is set in LocalDB.GetLocalDBConnectionString. We don't need to set Last here. + return null; + } + } + return localDBConnectionString; + } + + /// + /// Retrieves the part of the sqlUserInstance.dll from the registry + /// + /// In case the dll path is not found, the error is set here. + /// + private string GetUserInstanceDllPath(out LocalDBErrorState errorState) + { + using (TrySNIEventScope.Create(nameof(LocalDB))) + { + string dllPath = null; + using (RegistryKey key = Registry.LocalMachine.OpenSubKey(LocalDBInstalledVersionRegistryKey)) + { + if (key == null) + { + errorState = LocalDBErrorState.NO_INSTALLATION; + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.ERR, "No installation found."); + return null; + } + + Version zeroVersion = new Version(); + + Version latestVersion = zeroVersion; + + foreach (string subKey in key.GetSubKeyNames()) + { + Version currentKeyVersion; + + if (!Version.TryParse(subKey, out currentKeyVersion)) + { + errorState = LocalDBErrorState.INVALID_CONFIG; + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.ERR, "Invalid Configuration."); + return null; + } + + if (latestVersion.CompareTo(currentKeyVersion) < 0) + { + latestVersion = currentKeyVersion; + } + } + + // If no valid versions are found, then error out + if (latestVersion.Equals(zeroVersion)) + { + errorState = LocalDBErrorState.INVALID_CONFIG; + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.ERR, "Invalid Configuration."); + return null; + } + + // Use the latest version to get the DLL path + using (RegistryKey latestVersionKey = key.OpenSubKey(latestVersion.ToString())) + { + + object instanceAPIPathRegistryObject = latestVersionKey.GetValue(InstanceAPIPathValueName); + + if (instanceAPIPathRegistryObject == null) + { + errorState = LocalDBErrorState.NO_SQLUSERINSTANCEDLL_PATH; + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.ERR, "No SQL user instance DLL. Instance API Path Registry Object Error."); + return null; + } + + RegistryValueKind valueKind = latestVersionKey.GetValueKind(InstanceAPIPathValueName); + + if (valueKind != RegistryValueKind.String) + { + errorState = LocalDBErrorState.INVALID_SQLUSERINSTANCEDLL_PATH; + SqlClientEventSource.Log.TryTraceEvent(nameof(LocalDB), EventType.ERR, "Invalid SQL user instance DLL path. Registry value kind mismatch."); + return null; + } + + dllPath = (string)instanceAPIPathRegistryObject; + + errorState = LocalDBErrorState.NONE; + return dllPath; + } + } + } + } + + internal static string MapLocalDBErrorStateToErrorMessage(LocalDBErrorState errorState) + { + switch (errorState) + { + case LocalDBErrorState.NO_INSTALLATION: + return Strings.SNI_ERROR_52; + case LocalDBErrorState.INVALID_CONFIG: + return Strings.SNI_ERROR_53; + case LocalDBErrorState.NO_SQLUSERINSTANCEDLL_PATH: + return Strings.SNI_ERROR_54; + case LocalDBErrorState.INVALID_SQLUSERINSTANCEDLL_PATH: + return Strings.SNI_ERROR_55; + case LocalDBErrorState.NONE: + return Strings.SNI_ERROR_50; + default: + return Strings.SNI_ERROR_53; + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.Windows.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.Windows.cs index 8d276cd285..9844a96da3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.Windows.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserSafeHandles.Windows.cs @@ -76,7 +76,6 @@ override protected bool ReleaseHandle() { if (TdsEnums.SNI_SUCCESS == _sniStatus) { - LocalDBAPI.ReleaseDLLHandles(); SNINativeMethodWrapper.SNITerminate(); } base.handle = IntPtr.Zero; diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/LocalDBTest/LocalDBTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/LocalDBTest/LocalDBTest.cs index 0c06e6cf47..3987073af8 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/LocalDBTest/LocalDBTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/LocalDBTest/LocalDBTest.cs @@ -23,8 +23,7 @@ private enum InfoType private static readonly string s_commandPrompt = "cmd.exe"; private static readonly string s_sqlLocalDbInfo = @$"/c SqlLocalDb info {DataTestUtility.LocalDbAppName}"; private static readonly string s_startLocalDbCommand = @$"/c SqlLocalDb start {DataTestUtility.LocalDbAppName}"; - private static readonly string s_localDbNamedPipeConnectionString = @$"server={GetLocalDbNamedPipe()}"; - + #region LocalDbTests [SkipOnTargetFramework(TargetFrameworkMonikers.Uap)] // No Registry support on UAP [ConditionalFact(nameof(IsLocalDBEnvironmentSet))] @@ -100,7 +99,7 @@ public static void SqlLocalDbSharedInstanceConnectionTest() [ActiveIssue(20245)] //pending pipeline configuration public static void SqlLocalDbNamedPipeConnectionTest() { - ConnectionTest(s_localDbNamedPipeConnectionString); + ConnectionTest(@$"server={GetLocalDbNamedPipe()}"); } [Fact] @@ -110,7 +109,7 @@ public static void LocalDBNamedPipeEncryptionNotSupportedTest() { // Encryption is not supported by SQL Local DB. // But connection should succeed as encryption is disabled by driver. - ConnectionWithEncryptionTest(s_localDbNamedPipeConnectionString); + ConnectionWithEncryptionTest(@$"server={GetLocalDbNamedPipe()}"); } [Fact] @@ -118,11 +117,22 @@ public static void LocalDBNamedPipeEncryptionNotSupportedTest() [ActiveIssue(20245)] //pending pipeline configuration public static void LocalDBNamepipeMarsTest() { - ConnectionWithMarsTest(s_localDbNamedPipeConnectionString); + ConnectionWithMarsTest(@$"server={GetLocalDbNamedPipe()}"); } #endregion + [PlatformSpecific(TestPlatforms.AnyUnix)] + [Fact] + public static void SqlLocalDbNotSupportedException() + { + Assert.Throws(() => + { + using SqlConnection connection = new(s_localDbConnectionString); + connection.Open(); + }); + } + private static void ConnectionWithMarsTest(string connectionString) { SqlConnectionStringBuilder builder = new(connectionString)