#pragma warning disable 1634, 1691 /********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Collections.Generic; using System.Transactions; using System.Management.Automation.Internal; namespace System.Management.Automation { /// /// The status of a PowerShell transaction. /// public enum PSTransactionStatus { /// /// The transaction has been rolled back /// RolledBack = 0, /// /// The transaction has been committed /// Committed = 1, /// /// The transaction is currently active /// Active = 2 } /// /// Represents an active transaction /// /// public sealed class PSTransaction : IDisposable { /// /// Initializes a new instance of the PSTransaction class /// /// internal PSTransaction(RollbackSeverity rollbackPreference, TimeSpan timeout) { transaction = new CommittableTransaction(timeout); this.rollbackPreference = rollbackPreference; this.subscriberCount = 1; } /// /// Initializes a new instance of the PSTransaction class using a CommittableTransaction /// /// internal PSTransaction(CommittableTransaction transaction, RollbackSeverity severity) { this.transaction = transaction; this.rollbackPreference = severity; this.subscriberCount = 1; } private CommittableTransaction transaction; /// /// Gets the rollback preference for this transaction /// /// public RollbackSeverity RollbackPreference { get { return rollbackPreference; } } private RollbackSeverity rollbackPreference; /// /// Gets the number of subscribers to this transaction /// /// public int SubscriberCount { get { // Verify the transaction hasn't been rolled back beneath us if (this.IsRolledBack) { this.SubscriberCount = 0; } return subscriberCount; } set { subscriberCount = value; } } private int subscriberCount; /// /// Returns the status of this transaction. /// /// public PSTransactionStatus Status { get { if(IsRolledBack) { return PSTransactionStatus.RolledBack; } else if(IsCommitted) { return PSTransactionStatus.Committed; } else { return PSTransactionStatus.Active; } } } /// /// Activates the transaction held by this PSTransaction /// /// internal void Activate() { Transaction.Current = transaction; } /// /// Commits the transaction held by this PSTransaction /// /// internal void Commit() { transaction.Commit(); this.isCommitted = true; } /// /// Rolls back the transaction held by this PSTransaction /// /// internal void Rollback() { transaction.Rollback(); this.isRolledBack = true; } /// /// Determines whether this PSTransaction has been /// rolled back or not. /// /// internal bool IsRolledBack { get { // Check if it's been aborted underneath us if ( (! isRolledBack) && (transaction != null) && (transaction.TransactionInformation.Status == TransactionStatus.Aborted)) { isRolledBack = true; } return isRolledBack; } set { isRolledBack = value; } } private bool isRolledBack = false; /// /// Determines whether this PSTransaction /// has been committed or not. /// /// internal bool IsCommitted { get { return isCommitted; } set { isCommitted = value; } } private bool isCommitted = false; /// /// Destructor for the PSTransaction class /// /// ~PSTransaction() { Dispose(false); } /// /// Disposes the PSTransaction object. /// /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Disposes the PSTransaction object, which disposes the /// underlying transaction. /// /// /// Whether to actually dispose the object. /// /// /// public void Dispose(bool disposing) { if(disposing) { if(transaction != null) { transaction.Dispose(); } } } } /// /// Supports the transaction management infrastructure for the PowerShell engine /// /// public sealed class PSTransactionContext : IDisposable { /// /// Initializes a new instance of the PSTransactionManager class /// /// internal PSTransactionContext(PSTransactionManager transactionManager) { this.transactionManager = transactionManager; transactionManager.SetActive(); } private PSTransactionManager transactionManager; /// /// Destructor for the PSTransactionManager class /// /// ~PSTransactionContext() { Dispose(false); } /// /// Disposes the PSTransactionContext object. /// /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Disposes the PSTransactionContext object, which resets the /// active PSTransaction. /// /// /// Whether to actually dispose the object. /// /// /// private void Dispose(bool disposing) { if(disposing) { transactionManager.ResetActive(); } } } /// /// The severity of error that causes PowerShell to automatically /// rollback the transaction. /// public enum RollbackSeverity { /// /// Non-terminating errors or worse /// Error, /// /// Terminating errors or worse /// TerminatingError, /// /// Do not rollback the transaction on error /// Never } } namespace System.Management.Automation.Internal { /// /// Supports the transaction management infrastructure for the PowerShell engine /// /// internal sealed class PSTransactionManager : IDisposable { /// /// Initializes a new instance of the PSTransactionManager class /// /// internal PSTransactionManager() { transactionStack = new Stack(); transactionStack.Push(null); } /// /// Called by engine APIs to ensure they are protected from /// ambient transactions. /// /// internal static IDisposable GetEngineProtectionScope() { if(engineProtectionEnabled && (Transaction.Current != null)) { return new System.Transactions.TransactionScope( System.Transactions.TransactionScopeOption.Suppress); } else { return null; } } /// /// Called by the transaction manager to enable engine /// protection the first time a transaction is activated. /// Engine protection APIs remain protected from this point on. /// /// internal static void EnableEngineProtection() { engineProtectionEnabled = true; } private static bool engineProtectionEnabled = false; /// /// Gets the rollback preference for the active transaction /// /// internal RollbackSeverity RollbackPreference { get { PSTransaction currentTransaction = transactionStack.Peek(); if (currentTransaction == null) { string error = TransactionStrings.NoTransactionActive; // This is not an expected condition, and is just protective // coding. #pragma warning suppress 56503 throw new InvalidOperationException(error); } return currentTransaction.RollbackPreference; } } /// /// Creates a new Transaction if none are active. Otherwise, increments /// the subscriber count for the active transaction. /// /// internal void CreateOrJoin() { CreateOrJoin(RollbackSeverity.Error, TimeSpan.FromMinutes(1)); } /// /// Creates a new Transaction if none are active. Otherwise, increments /// the subscriber count for the active transaction. /// /// internal void CreateOrJoin(RollbackSeverity rollbackPreference, TimeSpan timeout) { PSTransaction currentTransaction = transactionStack.Peek(); // There is a transaction on the stack if (currentTransaction != null) { // If you are already in a transaction that has been aborted, or committed, // create it. if (currentTransaction.IsRolledBack || currentTransaction.IsCommitted) { // Clean up the "used" one transactionStack.Pop().Dispose(); // And add a new one to the stack transactionStack.Push(new PSTransaction(rollbackPreference, timeout)); } else { // This is a usable one. Add a subscriber to it. currentTransaction.SubscriberCount++; } } else { // Add a new transaction to the stack transactionStack.Push(new PSTransaction(rollbackPreference, timeout)); } } /// /// Creates a new Transaction that should be managed idependently of /// any parent transactions. /// /// internal void CreateNew() { CreateNew(RollbackSeverity.Error, TimeSpan.FromMinutes(1)); } /// /// Creates a new Transaction that should be managed idependently of /// any parent transactions. /// /// internal void CreateNew(RollbackSeverity rollbackPreference, TimeSpan timeout) { transactionStack.Push(new PSTransaction(rollbackPreference, timeout)); } /// /// Completes the current transaction. If only one subscriber is active, this /// commits the transaction. Otherwise, it reduces the subscriber count by one. /// /// internal void Commit() { PSTransaction currentTransaction = transactionStack.Peek(); // Should not be able to commit a transaction that is not active if (currentTransaction == null) { string error = TransactionStrings.NoTransactionActiveForCommit; throw new InvalidOperationException(error); } // If you are already in a transaction that has been aborted if (currentTransaction.IsRolledBack) { string error = TransactionStrings.TransactionRolledBackForCommit; throw new TransactionAbortedException(error); } // If you are already in a transaction that has been committed if (currentTransaction.IsCommitted) { string error = TransactionStrings.CommittedTransactionForCommit; throw new InvalidOperationException(error); } if(currentTransaction.SubscriberCount == 1) { currentTransaction.Commit(); currentTransaction.SubscriberCount = 0; } else { currentTransaction.SubscriberCount--; } // Now that we've committed, go back to the last available transaction while((transactionStack.Count > 2) && (transactionStack.Peek().IsRolledBack || transactionStack.Peek().IsCommitted)) { transactionStack.Pop().Dispose(); } } /// /// Aborts the current transaction, no matter how many subscribers are part of it. /// /// internal void Rollback() { Rollback(false); } /// /// Aborts the current transaction, no matter how many subscribers are part of it. /// /// internal void Rollback(bool suppressErrors) { PSTransaction currentTransaction = transactionStack.Peek(); // Should not be able to roll back a transaction that is not active if (currentTransaction == null) { string error = TransactionStrings.NoTransactionActiveForRollback; throw new InvalidOperationException(error); } // If you are already in a transaction that has been aborted if (currentTransaction.IsRolledBack) { if(! suppressErrors) { // Otherwise, you should not be able to roll it back. string error = TransactionStrings.TransactionRolledBackForRollback; throw new TransactionAbortedException(error); } } // See if they've already committed the transaction if (currentTransaction.IsCommitted) { if(! suppressErrors) { string error = TransactionStrings.CommittedTransactionForRollback; throw new InvalidOperationException(error); } } // Roll back the transaction if it hasn't been rolled back currentTransaction.SubscriberCount = 0; currentTransaction.Rollback(); // Now that we've rolled back, go back to the last available transaction while((transactionStack.Count > 2) && (transactionStack.Peek().IsRolledBack || transactionStack.Peek().IsCommitted)) { transactionStack.Pop().Dispose(); } } /// /// Sets the base transaction; any transactions created thereafter will be nested to this instance /// /// internal void SetBaseTransaction(CommittableTransaction transaction, RollbackSeverity severity) { if (this.HasTransaction) { throw new InvalidOperationException(TransactionStrings.BaseTransactionMustBeFirst); } PSTransaction currentTransaction = transactionStack.Peek(); // If there is a "used" transaction at the top of the stack, clean it up while(transactionStack.Peek() != null && (transactionStack.Peek().IsRolledBack || transactionStack.Peek().IsCommitted)) { transactionStack.Pop().Dispose(); } this.baseTransaction = new PSTransaction(transaction, severity); this.transactionStack.Push(this.baseTransaction); } /// /// Removes the transaction added by SetBaseTransaction /// /// internal void ClearBaseTransaction() { if (this.baseTransaction == null) { throw new InvalidOperationException(TransactionStrings.BaseTransactionNotSet); } if (this.transactionStack.Peek() != this.baseTransaction) { throw new InvalidOperationException(TransactionStrings.BaseTransactionNotActive); } this.transactionStack.Pop().Dispose(); this.baseTransaction = null; } private Stack transactionStack; private PSTransaction baseTransaction; /// /// Returns the current engine transaction /// /// internal PSTransaction GetCurrent() { return transactionStack.Peek(); } /// /// Activates the current transaction, both in the engine, and in the Ambient. /// /// internal void SetActive() { PSTransactionManager.EnableEngineProtection(); PSTransaction currentTransaction = transactionStack.Peek(); // Should not be able to activate a transaction that is not active if (currentTransaction == null) { string error = TransactionStrings.NoTransactionForActivation; throw new InvalidOperationException(error); } // If you are already in a transaction that has been aborted, you should // not be able to activate it. if (currentTransaction.IsRolledBack) { string error = TransactionStrings.NoTransactionForActivationBecauseRollback; throw new TransactionAbortedException(error); } previousActiveTransaction = Transaction.Current; currentTransaction.Activate(); } private Transaction previousActiveTransaction; /// /// Deactivates the current transaction in the engine, and restores the /// ambient transaction. /// /// internal void ResetActive() { // Even if you are in a transaction that has been aborted, you // should still be able to restore the current transaction. Transaction.Current = previousActiveTransaction; previousActiveTransaction = null; } /// /// Determines if you have a transaction that you can set active and work on. /// /// internal bool HasTransaction { get { PSTransaction currentTransaction = transactionStack.Peek(); if ((currentTransaction != null) && (!currentTransaction.IsCommitted) && (!currentTransaction.IsRolledBack)) { return true; } else { return false; } } } /// /// Determines if the last transaction has been committed. /// internal bool IsLastTransactionCommitted { get { PSTransaction currentTransaction = transactionStack.Peek(); if (currentTransaction != null) { return currentTransaction.IsCommitted; } else { return false; } } } /// /// Determines if the last transaction has been rolled back. /// internal bool IsLastTransactionRolledBack { get { PSTransaction currentTransaction = transactionStack.Peek(); if (currentTransaction != null) { return currentTransaction.IsRolledBack; } else { return false; } } } /// /// Destructor for the PSTransactionManager class /// /// ~PSTransactionManager() { Dispose(false); } /// /// Disposes the PSTransactionManager object. /// /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Disposes the PSTransactionContext object, which resets the /// active PSTransaction. /// /// /// Whether to actually dispose the object. /// /// /// [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "baseTransaction", Justification = "baseTransaction should not be disposed since we do not own it - it belongs to the caller")] public void Dispose(bool disposing) { if(disposing) { ResetActive(); while(transactionStack.Peek() != null) { PSTransaction currentTransaction = transactionStack.Pop(); if (currentTransaction != this.baseTransaction) { currentTransaction.Dispose(); } } } } } }