using System; using System.Threading; using System.Threading.Tasks; using Technosoftware.DaAeHdaClient.Utilities; namespace Technosoftware.DaAeHdaClient.Com.Utilities { /// /// The result of DCOM watchdog /// public enum DCOMWatchdogResult { /// /// Watchdog has not been set/there is no result /// None = 0, /// /// The Set/Reset cycle was manually completed i.e. the DCOM call did not timeout /// Completed, /// /// No Reset call occurred with the timeout period thus the current DCOM call was automatically cancelled /// TimedOut, /// /// The current DCOM call was manually cancelled /// ManuallyCancelled } /// /// Watchdog mechanism to allow for cancellation of DCOM calls. Note that this mechanism will only work for a STA thread apartment - the thread on which /// the watchdog is Set and DCOM calls are made have to be the same thread and the thread apartment model has to be set to STA. /// public static class DCOMCallWatchdog { #region Fields private const int DEFAULT_TIMEOUT_SECONDS = 10; private static object watchdogLock_ = new object(); private static uint watchDogThreadID_; private static bool isCancelled_; private static TimeSpan timeout_ = TimeSpan.Zero; //disabled by default private static Task watchdogTask_; private static DCOMWatchdogResult lastWatchdogResult_ = DCOMWatchdogResult.None; private static DateTime setStart_; #endregion #region Properties /// /// The result of the last watchdog set/reset operation /// public static DCOMWatchdogResult LastWatchdogResult { get { return lastWatchdogResult_; } } /// /// The current native thread ID on which the watchdog has been enabled /// public static uint WatchDogThreadID { get => watchDogThreadID_; } /// /// Indicates if the watchdog mechanism is active or not /// public static bool IsEnabled { get => timeout_ != TimeSpan.Zero; } /// /// Indicates if the watchdog has been set and is busy waiting for a call completion Reset to be called or a timeout to occur. /// public static bool IsSet { get => WatchDogThreadID != 0; } /// /// Indicates if the watchdog was cancelled due to a timeout /// public static bool IsCancelled { get => isCancelled_; } /// /// The watchdog timeout timespan /// public static TimeSpan Timeout { get => timeout_; set { Enable(value); } } #endregion #region Methods /// /// Enables the Watchdog mechanism. This can be called from any thread and does not have to be the DCOM call originator thread. /// Uses the default call timeout. /// public static void Enable() { Enable(TimeSpan.FromSeconds(DEFAULT_TIMEOUT_SECONDS)); } /// /// Enables the Watchdog mechanism. This can be called from any thread and does not have to be the DCOM call originator thread. /// /// The maximum time to wait for a DCOM call to succeed before it is cancelled. Note that DCOM will typically timeout /// between 1-2 minutes, depending on the OS public static void Enable(TimeSpan timeout) { if (timeout == TimeSpan.Zero) { timeout = TimeSpan.FromSeconds(DEFAULT_TIMEOUT_SECONDS); } lock (watchdogLock_) { timeout_ = timeout; } watchdogTask_ = Task.Run(() => WatchdogTask()); } /// /// Disables the watchdog mechanism and stops any call cancellations. /// /// True if enabled and now disabled, otherwise false public static bool Disable() { lock (watchdogLock_) { if (IsEnabled) { timeout_ = TimeSpan.Zero; return true; } else { return false; } } } /// /// Sets the watchdog timer active on the current thread. If Reset is not called within the timeout period, any current thread DCOM call will be cancelled. The /// calling thread must be the originator of the DCOM call and must be an STA thread. /// /// True if the watchdog set succeeds or was already set for the current thread, else false if the watchdog is not enabled. public static bool Set() { if (IsEnabled) { var apartmentState = Thread.CurrentThread.GetApartmentState(); if (apartmentState != ApartmentState.STA) { throw new InvalidOperationException("COM calls can only be cancelled on a COM STA apartment thread - use [STAThread] attibute or set the state of the thread on creation"); } lock (watchdogLock_) { var threadId = Interop.GetCurrentThreadId(); if (IsSet) { if (threadId != watchDogThreadID_) { throw new InvalidOperationException($"Attempt to set call cancellation on different thread [{threadId}] to where it was already enabled [{watchDogThreadID_}]"); } } else { isCancelled_ = false; watchDogThreadID_ = 0; lastWatchdogResult_ = DCOMWatchdogResult.None; //enable DCOM call cancellation for duration of the watchdog var hresult = Interop.CoEnableCallCancellation(IntPtr.Zero); if (hresult == 0) { setStart_ = DateTime.UtcNow; watchDogThreadID_ = threadId; Utils.Trace(Utils.TraceMasks.Information, $"COM call cancellation on thread [{watchDogThreadID_}] was set with timeout [{timeout_.TotalSeconds} seconds]"); } else { throw new Exception($"Failed to set COM call cancellation (HResult = {hresult})"); } } } return true; } else { return false; } } /// /// Refreshes the watchdog activity timer to now, effectively resetting the time to wait. /// /// True if the watchdog time was updated, else False if the watchdog timer is not Enabled or Set public static bool Update() { if (IsEnabled) { lock (watchdogLock_) { if (IsSet) { setStart_ = DateTime.UtcNow; return true; } else { return false; } } } else { return false; } } /// /// Resets the watchdog timer for the current thread. This should be called after a DCOM call returns to indicate the call succeeded, and thus cancelling the /// watchdog timer. /// /// True if the watchdog timer was reset for the current thread, else False if the timer was not set for the thread of the watchdog is not enabled. public static bool Reset() { if (IsEnabled) { lock (watchdogLock_) { if (IsSet) { var threadId = Interop.GetCurrentThreadId(); if (threadId == watchDogThreadID_) { if (!IsCancelled) { lastWatchdogResult_ = DCOMWatchdogResult.Completed; } watchDogThreadID_ = 0; isCancelled_ = false; //disable DCOM call cancellation var hresult = Interop.CoDisableCallCancellation(IntPtr.Zero); Utils.Trace(Utils.TraceMasks.Information, $"COM call cancellation on thread [{watchDogThreadID_}] was reset [HRESULT = {hresult}]"); } else { throw new Exception($"COM call cancellation cannot be reset from different thread [{threadId}] it was set on [{watchDogThreadID_}]"); } } } return true; } else { return false; } } /// /// Allows for manual cancellation of the current DCOM call /// /// public static bool Cancel() { return Cancel(DCOMWatchdogResult.ManuallyCancelled); } /// /// Cancels the current DCOM call if there is one active /// /// /// The reason for the cancellation private static bool Cancel(DCOMWatchdogResult reason) { if (IsEnabled) { lock (watchdogLock_) { if (!IsCancelled && IsSet) { isCancelled_ = true; //cancel the current DCOM call immediately var hresult = Interop.CoCancelCall(watchDogThreadID_, 0); Utils.Trace(Utils.TraceMasks.Information, $"COM call on thread [{watchDogThreadID_}] was cancelled [HRESULT = {hresult}]"); lastWatchdogResult_ = reason; return true; } else { return false; } } } else { return false; } } /// /// The Watchdog Task is a seperate thread that is activated when the Watchdog is enabled. It checks the time since the last Set was called and /// then cancels the current DCOM call automatically if Reset is not called within the timeout period. /// private static void WatchdogTask() { while (IsEnabled) { try { if (IsSet & !IsCancelled) { if (TimeElapsed(setStart_) >= timeout_) { Utils.Trace(Utils.TraceMasks.Information, $"Sync call watchdog for thread [{watchDogThreadID_}] timed out - cancelling current call..."); Cancel(DCOMWatchdogResult.TimedOut); } } } catch (Exception e) { Utils.Trace(Utils.TraceMasks.Error, $"Error in Sync call watchdog thread : {e.ToString()}"); } finally { Thread.Sleep(1); } } } private static TimeSpan TimeElapsed(DateTime startTime) { var now = DateTime.UtcNow; startTime = startTime.ToUniversalTime(); if (startTime > now) { return startTime - now; } else { return now - startTime; } } #endregion } }