// Copyright ?2004, 2014, Oracle and/or its affiliates. All rights reserved. // // MySQL Connector/NET is licensed under the terms of the GPLv2 // , like most // MySQL Connectors. There are special exceptions to the terms and // conditions of the GPLv2 as it is applied to this software, see the // FLOSS License Exception // . // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published // by the Free Software Foundation; version 2 of the License. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License // for more details. // // You should have received a copy of the GNU General Public License along // with this program; if not, write to the Free Software Foundation, Inc., // 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #if MYSQL_6_9 using System; using System.ComponentModel; using System.Data; using System.Data.Common; using System.Transactions; using IsolationLevel = System.Data.IsolationLevel; using System.Drawing; using System.Drawing.Design; using System.Text; using Externals.MySql.Data.Common; using System.Diagnostics; using Externals.MySql.Data.MySqlClient.Properties; using Externals.MySql.Data.MySqlClient.Replication; using System.Threading.Tasks; using System.Threading; namespace Externals.MySql.Data.MySqlClient { internal sealed partial class MySqlConnection : IDisposable { internal ConnectionState connectionState; internal Driver driver; internal bool hasBeenOpen; private SchemaProvider schemaProvider; private ProcedureCache procedureCache; private bool isInUse; private PerformanceMonitor perfMonitor; private ExceptionInterceptor exceptionInterceptor; internal CommandInterceptor commandInterceptor; private bool isKillQueryConnection; private string database; private int commandTimeout; public event MySqlInfoMessageEventHandler InfoMessage; private static Cache connectionStringCache = new Cache(0, 25); public MySqlConnection() { //TODO: add event data to StateChange docs Settings = new MySqlConnectionStringBuilder(); database = String.Empty; } public MySqlConnection(string connectionString) : this() { ConnectionString = connectionString; } #region Destructor ~MySqlConnection() { Dispose(false); } #endregion #region Interal Methods & Properties internal PerformanceMonitor PerfMonitor { get { return perfMonitor; } } internal ProcedureCache ProcedureCache { get { return procedureCache; } } internal MySqlConnectionStringBuilder Settings { get; private set; } internal MySqlDataReader Reader { get { if (driver == null) return null; return driver.reader; } set { driver.reader = value; isInUse = driver.reader != null; } } internal void OnInfoMessage(MySqlInfoMessageEventArgs args) { if (InfoMessage != null) { InfoMessage(this, args); } } internal bool SoftClosed { get { return (State == ConnectionState.Closed) && driver != null && driver.CurrentTransaction != null; } } internal bool IsInUse { get { return isInUse; } set { isInUse = value; } } #endregion #region Properties /// /// Returns the id of the server thread this connection is executing on /// [Browsable(false)] public int ServerThread { get { return driver.ThreadID; } } /// /// Gets the name of the MySQL server to which to connect. /// [Browsable(true)] public override string DataSource { get { return Settings.Server; } } [Browsable(true)] public override int ConnectionTimeout { get { return (int)Settings.ConnectionTimeout; } } [Browsable(true)] public override string Database { get { return database; } } /// /// Indicates if this connection should use compression when communicating with the server. /// [Browsable(false)] public bool UseCompression { get { return Settings.UseCompression; } } [Browsable(false)] public override ConnectionState State { get { return connectionState; } } [Browsable(false)] public override string ServerVersion { get { return driver.Version.ToString(); } } [Editor("Externals.MySql.Data.MySqlClient.Design.ConnectionStringTypeEditor,MySqlClient.Design", typeof(UITypeEditor))] [Browsable(true)] [Category("Data")] [Description( "Information used to connect to a DataSource, such as 'Server=xxx;UserId=yyy;Password=zzz;Database=dbdb'.")] public override string ConnectionString { get { // Always return exactly what the user set. // Security-sensitive information may be removed. return Settings.GetConnectionString(!hasBeenOpen || Settings.PersistSecurityInfo); } set { if (State != ConnectionState.Closed) Throw(new MySqlException( "Not allowed to change the 'ConnectionString' property while the connection (state=" + State + ").")); MySqlConnectionStringBuilder newSettings; lock (connectionStringCache) { if (value == null) newSettings = new MySqlConnectionStringBuilder(); else { newSettings = (MySqlConnectionStringBuilder)connectionStringCache[value]; if (null == newSettings) { newSettings = new MySqlConnectionStringBuilder(value); connectionStringCache.Add(value, newSettings); } } } Settings = newSettings; if (Settings.Database != null && Settings.Database.Length > 0) this.database = Settings.Database; if (driver != null) driver.Settings = newSettings; } } protected override DbProviderFactory DbProviderFactory { get { return MySqlClientFactory.Instance; } } public bool IsPasswordExpired { get { return driver.IsPasswordExpired; } } #endregion partial void AssertPermissions(); #region Transactions /// /// Enlists in the specified transaction. /// /// /// A reference to an existing in which to enlist. /// public override void EnlistTransaction(Transaction transaction) { // enlisting in the null transaction is a noop if (transaction == null) return; // guard against trying to enlist in more than one transaction if (driver.CurrentTransaction != null) { if (driver.CurrentTransaction.BaseTransaction == transaction) return; Throw(new MySqlException("Already enlisted")); } // now see if we need to swap out drivers. We would need to do this since // we have to make sure all ops for a given transaction are done on the // same physical connection. Driver existingDriver = DriverTransactionManager.GetDriverInTransaction(transaction); if (existingDriver != null) { // we can't allow more than one driver to contribute to the same connection if (existingDriver.IsInActiveUse) Throw(new NotSupportedException(Resources.MultipleConnectionsInTransactionNotSupported)); // there is an existing driver and it's not being currently used. // now we need to see if it is using the same connection string string text1 = existingDriver.Settings.ConnectionString; string text2 = Settings.ConnectionString; if (String.Compare(text1, text2, true) != 0) Throw(new NotSupportedException(Resources.MultipleConnectionsInTransactionNotSupported)); // close existing driver // set this new driver as our existing driver CloseFully(); driver = existingDriver; } if (driver.CurrentTransaction == null) { MySqlPromotableTransaction t = new MySqlPromotableTransaction(this, transaction); if (!transaction.EnlistPromotableSinglePhase(t)) Throw(new NotSupportedException(Resources.DistributedTxnNotSupported)); driver.CurrentTransaction = t; DriverTransactionManager.SetDriverInTransaction(driver); driver.IsInActiveUse = true; } } public new MySqlTransaction BeginTransaction() { return BeginTransaction(IsolationLevel.RepeatableRead); } public new MySqlTransaction BeginTransaction(IsolationLevel iso) { //TODO: check note in help if (State != ConnectionState.Open) Throw(new InvalidOperationException(Resources.ConnectionNotOpen)); // First check to see if we are in a current transaction if (driver.HasStatus(ServerStatusFlags.InTransaction)) Throw(new InvalidOperationException(Resources.NoNestedTransactions)); MySqlTransaction t = new MySqlTransaction(this, iso); MySqlCommand cmd = new MySqlCommand("", this); cmd.CommandText = "SET SESSION TRANSACTION ISOLATION LEVEL "; switch (iso) { case IsolationLevel.ReadCommitted: cmd.CommandText += "READ COMMITTED"; break; case IsolationLevel.ReadUncommitted: cmd.CommandText += "READ UNCOMMITTED"; break; case IsolationLevel.RepeatableRead: cmd.CommandText += "REPEATABLE READ"; break; case IsolationLevel.Serializable: cmd.CommandText += "SERIALIZABLE"; break; case IsolationLevel.Chaos: Throw(new NotSupportedException(Resources.ChaosNotSupported)); break; case IsolationLevel.Snapshot: Throw(new NotSupportedException(Resources.SnapshotNotSupported)); break; } cmd.ExecuteNonQuery(); cmd.CommandText = "BEGIN"; cmd.ExecuteNonQuery(); return t; } #endregion public override void ChangeDatabase(string databaseName) { if (databaseName == null || databaseName.Trim().Length == 0) Throw(new ArgumentException(Resources.ParameterIsInvalid, "databaseName")); if (State != ConnectionState.Open) Throw(new InvalidOperationException(Resources.ConnectionNotOpen)); // This lock prevents promotable transaction rollback to run // in parallel lock (driver) { if (Transaction.Current != null && Transaction.Current.TransactionInformation.Status == TransactionStatus.Aborted) { Throw(new TransactionAbortedException()); } // We use default command timeout for SetDatabase using (new CommandTimer(this, (int)Settings.DefaultCommandTimeout)) { driver.SetDatabase(databaseName); } } this.database = databaseName; } internal void SetState(ConnectionState newConnectionState, bool broadcast) { if (newConnectionState == connectionState && !broadcast) return; ConnectionState oldConnectionState = connectionState; connectionState = newConnectionState; if (broadcast) OnStateChange(new StateChangeEventArgs(oldConnectionState, connectionState)); } /// /// Ping /// /// public bool Ping() { if (Reader != null) Throw(new MySqlException(Resources.DataReaderOpen)); if (driver != null && driver.Ping()) return true; driver = null; SetState(ConnectionState.Closed, true); return false; } public override void Open() { if (State == ConnectionState.Open) Throw(new InvalidOperationException(Resources.ConnectionAlreadyOpen)); // start up our interceptors exceptionInterceptor = new ExceptionInterceptor(this); commandInterceptor = new CommandInterceptor(this); SetState(ConnectionState.Connecting, true); AssertPermissions(); // if we are auto enlisting in a current transaction, then we will be // treating the connection as pooled if (Settings.AutoEnlist && Transaction.Current != null) { driver = DriverTransactionManager.GetDriverInTransaction(Transaction.Current); if (driver != null && (driver.IsInActiveUse || !driver.Settings.EquivalentTo(this.Settings))) Throw(new NotSupportedException(Resources.MultipleConnectionsInTransactionNotSupported)); } try { MySqlConnectionStringBuilder currentSettings = Settings; // Load balancing if (ReplicationManager.IsReplicationGroup(Settings.Server)) { if (driver == null) { ReplicationManager.GetNewConnection(Settings.Server, false, this); } else currentSettings = driver.Settings; } if (Settings.Pooling) { MySqlPool pool = MySqlPoolManager.GetPool(currentSettings); if (driver == null || !driver.IsOpen) driver = pool.GetConnection(); procedureCache = pool.ProcedureCache; } else { if (driver == null || !driver.IsOpen) driver = Driver.Create(currentSettings); procedureCache = new ProcedureCache((int)Settings.ProcedureCacheSize); } } catch { SetState(ConnectionState.Closed, true); throw; } // if the user is using old syntax, let them know if (driver.Settings.UseOldSyntax) MySqlTrace.LogWarning(ServerThread, "You are using old syntax that will be removed in future versions"); SetState(ConnectionState.Open, false); driver.Configure(this); if (!(driver.SupportsPasswordExpiration && driver.IsPasswordExpired)) { if (Settings.Database != null && Settings.Database != String.Empty) ChangeDatabase(Settings.Database); } // setup our schema provider schemaProvider = new ISSchemaProvider(this); perfMonitor = new PerformanceMonitor(this); // if we are opening up inside a current transaction, then autoenlist // TODO: control this with a connection string option if (Transaction.Current != null && Settings.AutoEnlist) EnlistTransaction(Transaction.Current); hasBeenOpen = true; SetState(ConnectionState.Open, true); } public new MySqlCommand CreateCommand() { // Return a new instance of a command object. MySqlCommand c = new MySqlCommand(); c.Connection = this; return c; } /// /// Creates a new MySqlConnection object with the exact same ConnectionString value /// /// A cloned MySqlConnection object public object Clone() { MySqlConnection clone = new MySqlConnection(); string connectionString = Settings.ConnectionString; if (connectionString != null) clone.ConnectionString = connectionString; return clone; } internal void Abort() { try { driver.Close(); } catch (Exception ex) { MySqlTrace.LogWarning(ServerThread, String.Concat("Error occurred aborting the connection. Exception was: ", ex.Message)); } finally { this.isInUse = false; } SetState(ConnectionState.Closed, true); } internal void CloseFully() { if (Settings.Pooling && driver.IsOpen) { // if we are in a transaction, roll it back if (driver.HasStatus(ServerStatusFlags.InTransaction)) { MySqlTransaction t = new MySqlTransaction(this, IsolationLevel.Unspecified); t.Rollback(); } MySqlPoolManager.ReleaseConnection(driver); } else driver.Close(); driver = null; } public override void Close() { if (driver != null) driver.IsPasswordExpired = false; if (State == ConnectionState.Closed) return; if (Reader != null) Reader.Close(); // if the reader was opened with CloseConnection then driver // will be null on the second time through if (driver != null) { if (driver.CurrentTransaction == null) CloseFully(); else driver.IsInActiveUse = false; } SetState(ConnectionState.Closed, true); } internal string CurrentDatabase() { if (Database != null && Database.Length > 0) return Database; MySqlCommand cmd = new MySqlCommand("SELECT database()", this); return cmd.ExecuteScalar().ToString(); } internal void HandleTimeoutOrThreadAbort(Exception ex) { bool isFatal = false; if (isKillQueryConnection) { // Special connection started to cancel a query. // Abort will prevent recursive connection spawning Abort(); if (ex is TimeoutException) { Throw(new MySqlException(Resources.Timeout, true, ex)); } else { return; } } try { // Do a fast cancel.The reason behind small values for connection // and command timeout is that we do not want user to wait longer // after command has already expired. // Microsoft's SqlClient seems to be using 5 seconds timeouts // here as well. // Read the error packet with "interrupted" message. CancelQuery(5); driver.ResetTimeout(5000); if (Reader != null) { Reader.Close(); Reader = null; } } catch (Exception ex2) { MySqlTrace.LogWarning(ServerThread, "Could not kill query, " + " aborting connection. Exception was " + ex2.Message); Abort(); isFatal = true; } if (ex is TimeoutException) { Throw(new MySqlException(Resources.Timeout, isFatal, ex)); } } public void CancelQuery(int timeout) { MySqlConnectionStringBuilder cb = new MySqlConnectionStringBuilder( Settings.ConnectionString); cb.Pooling = false; cb.AutoEnlist = false; cb.ConnectionTimeout = (uint)timeout; using (MySqlConnection c = new MySqlConnection(cb.ConnectionString)) { c.isKillQueryConnection = true; c.Open(); string commandText = "KILL QUERY " + ServerThread; MySqlCommand cmd = new MySqlCommand(commandText, c); cmd.CommandTimeout = timeout; cmd.ExecuteNonQuery(); } } #region Routines for timeout support. // Problem description: // Sometimes, ExecuteReader is called recursively. This is the case if // command behaviors are used and we issue "set sql_select_limit" // before and after command. This is also the case with prepared // statements , where we set session variables. In these situations, we // have to prevent recursive ExecuteReader calls from overwriting // timeouts set by the top level command. // To solve the problem, SetCommandTimeout() and ClearCommandTimeout() are // introduced . Query timeout here is "sticky", that is once set with // SetCommandTimeout, it only be overwritten after ClearCommandTimeout // (SetCommandTimeout would return false if it timeout has not been // cleared). // The proposed usage pattern of there routines is following: // When timed operations starts, issue SetCommandTimeout(). When it // finishes, issue ClearCommandTimeout(), but _only_ if call to // SetCommandTimeout() was successful. /// /// Sets query timeout. If timeout has been set prior and not /// yet cleared ClearCommandTimeout(), it has no effect. /// /// timeout in seconds /// true if internal bool SetCommandTimeout(int value) { if (!hasBeenOpen) // Connection timeout is handled by driver return false; if (commandTimeout != 0) // someone is trying to set a timeout while command is already // running. It could be for example recursive call to ExecuteReader // Ignore the request, as only top-level (non-recursive commands) // can set timeouts. return false; if (driver == null) return false; commandTimeout = value; driver.ResetTimeout(commandTimeout * 1000); return true; } /// /// Clears query timeout, allowing next SetCommandTimeout() to succeed. /// internal void ClearCommandTimeout() { if (!hasBeenOpen) return; commandTimeout = 0; if (driver != null) { driver.ResetTimeout(0); } } #endregion public MySqlSchemaCollection GetSchemaCollection(string collectionName, string[] restrictionValues) { if (collectionName == null) collectionName = SchemaProvider.MetaCollection; string[] restrictions = schemaProvider.CleanRestrictions(restrictionValues); MySqlSchemaCollection c = schemaProvider.GetSchema(collectionName, restrictions); return c; } #region Pool Routines public static void ClearPool(MySqlConnection connection) { MySqlPoolManager.ClearPool(connection.Settings); } public static void ClearAllPools() { MySqlPoolManager.ClearAllPools(); } #endregion internal void Throw(Exception ex) { throw ex; } public new void Dispose() { Dispose(true); GC.SuppressFinalize(this); } #region Async /// /// Async version of BeginTransaction /// /// An object representing the new transaction. public Task BeginTransactionAsync() { return BeginTransactionAsync(IsolationLevel.RepeatableRead, CancellationToken.None); } public Task BeginTransactionAsync(CancellationToken cancellationToken) { return BeginTransactionAsync(IsolationLevel.RepeatableRead, cancellationToken); } /// /// Async version of BeginTransaction /// /// The isolation level under which the transaction should run. /// An object representing the new transaction. public Task BeginTransactionAsync(IsolationLevel iso) { return BeginTransactionAsync(iso, CancellationToken.None); } public Task BeginTransactionAsync(IsolationLevel iso, CancellationToken cancellationToken) { var result = new TaskCompletionSource(); if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested) { try { MySqlTransaction tranResult = BeginTransaction(iso); result.SetResult(tranResult); } catch (Exception ex) { result.SetException(ex); } } else { result.SetCanceled(); } return result.Task; } public Task ChangeDataBaseAsync(string databaseName) { return ChangeDataBaseAsync(databaseName, CancellationToken.None); } /// /// Async version of ChangeDataBase /// public Task ChangeDataBaseAsync(string databaseName, CancellationToken cancellationToken) { var result = new TaskCompletionSource(); if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested) { try { ChangeDatabase(databaseName); result.SetResult(true); } catch (Exception ex) { result.SetException(ex); } } return result.Task; } ///// ///// Async version of Open ///// ///// //public Task OpenAsync() //{ // return Task.Run(() => // { // Open(); // }); //} /// /// Async version of Close /// /// public Task CloseAsync() { return CloseAsync(CancellationToken.None); } public Task CloseAsync(CancellationToken cancellationToken) { var result = new TaskCompletionSource(); if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested) { try { Close(); result.SetResult(true); } catch (Exception ex) { result.SetException(ex); } } else { result.SetCanceled(); } return result.Task; } /// /// Async version of ClearPool /// /// The connection associated with the pool to be cleared. /// public Task ClearPoolAsync(MySqlConnection connection) { return ClearPoolAsync(connection, CancellationToken.None); } public Task ClearPoolAsync(MySqlConnection connection, CancellationToken cancellationToken) { var result = new TaskCompletionSource(); if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested) { try { ClearPool(connection); result.SetResult(true); } catch (Exception ex) { result.SetException(ex); } } else { result.SetCanceled(); } return result.Task; } /// /// Async version of ClearAllPools /// /// public Task ClearAllPoolsAsync() { return ClearAllPoolsAsync(CancellationToken.None); } public Task ClearAllPoolsAsync(CancellationToken cancellationToken) { var result = new TaskCompletionSource(); if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested) { try { ClearAllPools(); result.SetResult(true); } catch (Exception ex) { result.SetException(ex); } } else { result.SetCanceled(); } return result.Task; } /// /// Async version of GetSchemaCollection /// /// Name of the collection /// Values to restrict /// A schema collection public Task GetSchemaCollectionAsync(string collectionName, string[] restrictionValues) { return GetSchemaCollectionAsync(collectionName, restrictionValues, CancellationToken.None); } public Task GetSchemaCollectionAsync(string collectionName, string[] restrictionValues, CancellationToken cancellationToken) { var result = new TaskCompletionSource(); if (cancellationToken == CancellationToken.None || !cancellationToken.IsCancellationRequested) { try { var schema = GetSchemaCollection(collectionName, restrictionValues); result.SetResult(schema); } catch (Exception ex) { result.SetException(ex); } } else { result.SetCanceled(); } return result.Task; } #endregion } /// /// Represents the method that will handle the event of a /// . /// internal delegate void MySqlInfoMessageEventHandler(object sender, MySqlInfoMessageEventArgs args); /// /// Provides data for the InfoMessage event. This class cannot be inherited. /// internal class MySqlInfoMessageEventArgs : EventArgs { /// /// /// public MySqlError[] errors; } /// /// IDisposable wrapper around SetCommandTimeout and ClearCommandTimeout /// functionality /// internal class CommandTimer : IDisposable { bool timeoutSet; MySqlConnection connection; public CommandTimer(MySqlConnection connection, int timeout) { this.connection = connection; if (connection != null) { timeoutSet = connection.SetCommandTimeout(timeout); } } #region IDisposable Members public void Dispose() { if (timeoutSet) { timeoutSet = false; connection.ClearCommandTimeout(); connection = null; } } #endregion } } #endif