diff --git a/src/Lithnet.Miiserver.AutoSync.Setup/Components.wxs b/src/Lithnet.Miiserver.AutoSync.Setup/Components.wxs index 6f323e7..d703861 100644 --- a/src/Lithnet.Miiserver.AutoSync.Setup/Components.wxs +++ b/src/Lithnet.Miiserver.AutoSync.Setup/Components.wxs @@ -47,7 +47,7 @@ - + diff --git a/src/Lithnet.Miiserver.AutoSync/Config/RegistrySettings.cs b/src/Lithnet.Miiserver.AutoSync/Config/RegistrySettings.cs index 93094b7..0d7e517 100644 --- a/src/Lithnet.Miiserver.AutoSync/Config/RegistrySettings.cs +++ b/src/Lithnet.Miiserver.AutoSync/Config/RegistrySettings.cs @@ -223,7 +223,7 @@ public static TimeSpan ExecutionStaggerInterval } else { - return TimeSpan.FromSeconds(2); + return TimeSpan.FromSeconds(1); } } } @@ -238,6 +238,19 @@ public static TimeSpan ExclusiveModeDeferralInterval } } + public static int LockMode + { + get + { + // LockMode = 0 : FIFO locking + // LockMode = 1 : Exclusive jobs yield to any non-exclusive job that was in the queue at the time the exclusive job took the x-lock + // LockMode = 2 : Exclusive jobs yield to all non-exclusive jobs + + return (int)RegistrySettings.ParametersKey.GetValue(nameof(LockMode), 0); + } + } + + public static TimeSpan PostRunInterval { get diff --git a/src/Lithnet.Miiserver.AutoSync/EventArgs/ExecutionParameters.cs b/src/Lithnet.Miiserver.AutoSync/EventArgs/ExecutionParameters.cs index bc6814a..7034ee9 100644 --- a/src/Lithnet.Miiserver.AutoSync/EventArgs/ExecutionParameters.cs +++ b/src/Lithnet.Miiserver.AutoSync/EventArgs/ExecutionParameters.cs @@ -6,12 +6,16 @@ public class ExecutionParameters { public bool Exclusive { get; set; } + public bool RunImmediate { get; set; } + public string RunProfileName { get; set; } public MARunProfileType RunProfileType { get; set; } public Guid PartitionID { get; set; } + public long QueueID { get; set; } + public string PartitionName { get; set; } public ExecutionParameters() @@ -19,43 +23,51 @@ public ExecutionParameters() } public ExecutionParameters(string runProfileName) - : this(runProfileName, false) + : this(runProfileName, false, false) { } public ExecutionParameters(string runProfileName, bool exclusive) + : this(runProfileName, exclusive, false) + { + } + + public ExecutionParameters(string runProfileName, bool exclusive, bool runImmediate) { this.RunProfileName = runProfileName; this.Exclusive = exclusive; + this.RunImmediate = runImmediate; } public ExecutionParameters(MARunProfileType runProfileType) - : this(runProfileType, null, false) + : this(runProfileType, null, false, false) { } public ExecutionParameters(MARunProfileType runProfileType, Guid partitionID) - : this(runProfileType, partitionID, false) + : this(runProfileType, partitionID, false, false) { } - public ExecutionParameters(MARunProfileType runProfileType, bool exclusive) - : this(runProfileType, null, exclusive) + public ExecutionParameters(MARunProfileType runProfileType, bool exclusive, bool runImmediate) + : this(runProfileType, null, exclusive, runImmediate) { } - public ExecutionParameters(MARunProfileType runProfileType, Guid partitionID, bool exclusive) + public ExecutionParameters(MARunProfileType runProfileType, Guid partitionID, bool exclusive, bool runImmediate) { this.RunProfileType = runProfileType; this.PartitionID = partitionID; this.Exclusive = exclusive; + this.RunImmediate = runImmediate; } - public ExecutionParameters(MARunProfileType runProfileType, string partitionName, bool exclusive) + public ExecutionParameters(MARunProfileType runProfileType, string partitionName, bool exclusive, bool runImmediate) { this.RunProfileType = runProfileType; this.PartitionName = partitionName; this.Exclusive = exclusive; + this.RunImmediate = runImmediate; } public override bool Equals(object obj) diff --git a/src/Lithnet.Miiserver.AutoSync/EventArgs/ExecutionTriggerEventArgs.cs b/src/Lithnet.Miiserver.AutoSync/EventArgs/ExecutionTriggerEventArgs.cs index f9d90e5..383dfb6 100644 --- a/src/Lithnet.Miiserver.AutoSync/EventArgs/ExecutionTriggerEventArgs.cs +++ b/src/Lithnet.Miiserver.AutoSync/EventArgs/ExecutionTriggerEventArgs.cs @@ -33,7 +33,7 @@ public ExecutionTriggerEventArgs(MARunProfileType runProfileType, Guid partition public ExecutionTriggerEventArgs(MARunProfileType runProfileType, string partitionName) { - this.Parameters = new ExecutionParameters(runProfileType, partitionName, false); + this.Parameters = new ExecutionParameters(runProfileType, partitionName, false, false); } } } diff --git a/src/Lithnet.Miiserver.AutoSync/Lithnet.Miiserver.AutoSync.csproj b/src/Lithnet.Miiserver.AutoSync/Lithnet.Miiserver.AutoSync.csproj index 49a9c92..b1e92a7 100644 --- a/src/Lithnet.Miiserver.AutoSync/Lithnet.Miiserver.AutoSync.csproj +++ b/src/Lithnet.Miiserver.AutoSync/Lithnet.Miiserver.AutoSync.csproj @@ -23,7 +23,7 @@ full false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG;LOCKDEBUG prompt 4 false diff --git a/src/Lithnet.Miiserver.AutoSync/MAInterface/MAController.cs b/src/Lithnet.Miiserver.AutoSync/MAInterface/MAController.cs index cfaf118..1f6c2c4 100644 --- a/src/Lithnet.Miiserver.AutoSync/MAInterface/MAController.cs +++ b/src/Lithnet.Miiserver.AutoSync/MAInterface/MAController.cs @@ -16,11 +16,19 @@ internal class MAController { private const int MonitorLockWaitInterval = 100; + protected static long CurrentQueueID = 0; + + protected static long WaitingExclusiveOpID = 0; + private static Logger logger = LogManager.GetCurrentClassLogger(); protected static SemaphoreSlim GlobalStaggeredExecutionLock; protected static ManualResetEvent GlobalExclusiveOperationLock; protected static SemaphoreSlim GlobalExclusiveOperationLockSemaphore; + + protected static ManualResetEvent GlobalExclusiveOperationRunningLock; + protected static SemaphoreSlim GlobalExclusiveOperationRunningLockSemaphore; + protected static SemaphoreSlim GlobalSynchronizationStepLock; protected static ConcurrentDictionary AllMaLocalOperationLocks; @@ -160,6 +168,10 @@ static MAController() MAController.GlobalStaggeredExecutionLock = new SemaphoreSlim(1, 1); MAController.GlobalExclusiveOperationLockSemaphore = new SemaphoreSlim(1, 1); MAController.GlobalExclusiveOperationLock = new ManualResetEvent(true); + + MAController.GlobalExclusiveOperationRunningLockSemaphore = new SemaphoreSlim(1, 1); + MAController.GlobalExclusiveOperationRunningLock = new ManualResetEvent(true); + MAController.AllMaLocalOperationLocks = new ConcurrentDictionary(); } @@ -321,6 +333,12 @@ public void AttachTrigger(params IMAExecutionTrigger[] triggers) foreach (IMAExecutionTrigger trigger in triggers) { + if (trigger.Disabled) + { + this.LogInfo($"Skipping disabled trigger '{trigger.DisplayName}'"); + continue; + } + this.ExecutionTriggers.Add(trigger); } } @@ -438,7 +456,7 @@ private void QueueFollowupActions(RunDetails d) continue; } - this.AddPendingActionIfNotQueued(new ExecutionParameters(requiredAction.Item1.ConfirmingImportRunProfileName), d.RunProfileName, true); + this.AddPendingActionIfNotQueued(new ExecutionParameters(requiredAction.Item1.ConfirmingImportRunProfileName, false, true), d.RunProfileName); continue; } @@ -450,7 +468,7 @@ private void QueueFollowupActions(RunDetails d) continue; } - this.AddPendingActionIfNotQueued(new ExecutionParameters(requiredAction.Item1.DeltaSyncRunProfileName), d.RunProfileName, true); + this.AddPendingActionIfNotQueued(new ExecutionParameters(requiredAction.Item1.DeltaSyncRunProfileName, false, true), d.RunProfileName); continue; } } @@ -686,6 +704,7 @@ private void WaitAndTakeLock(SemaphoreSlim mre, string name, CancellationTokenSo { this.Debug($"LOCK: WAIT: {name}: {caller}"); mre.Wait(ts.Token); + ts.Token.ThrowIfCancellationRequested(); this.Debug($"LOCK: TAKE: {name}: {caller}"); } @@ -697,10 +716,12 @@ private void WaitAndTakeLockWithSemaphore(EventWaitHandle mre, SemaphoreSlim sem { this.Debug($"SYNCOBJECT: WAIT: {name}: {caller}"); sem.Wait(ts.Token); + ts.Token.ThrowIfCancellationRequested(); gotLock = true; this.Debug($"SYNCOBJECT: LOCKED: {name}: {caller}"); this.Wait(mre, name, ts); this.TakeLockUnsafe(mre, name, ts, caller); + ts.Token.ThrowIfCancellationRequested(); } finally { @@ -963,7 +984,7 @@ private void WaitOnUnmanagedRun() } bool hasLocalLock = false; - + bool hasRunLock = false; try { string erp = this.ma.ExecutingRunProfileName; @@ -988,6 +1009,15 @@ private void WaitOnUnmanagedRun() try { + if (Program.ActiveConfig.Settings.RunMode == RunMode.Exclusive || Program.ActiveConfig.Settings.RunMode == RunMode.Supported) + { + this.WaitAndTakeLockWithSemaphore(MAController.GlobalExclusiveOperationLock, MAController.GlobalExclusiveOperationLockSemaphore, nameof(MAController.GlobalExclusiveOperationLock), linkedToken); + this.HasExclusiveLock = true; + + this.WaitAndTakeLockWithSemaphore(MAController.GlobalExclusiveOperationRunningLock, MAController.GlobalExclusiveOperationRunningLockSemaphore, nameof(MAController.GlobalExclusiveOperationLock), linkedToken); + hasRunLock = true; + } + this.WaitAndTakeLock(MAController.GlobalSynchronizationStepLock, nameof(MAController.GlobalSynchronizationStepLock), linkedToken); this.HasSyncLock = true; this.ma.Wait(linkedToken.Token); @@ -999,6 +1029,18 @@ private void WaitOnUnmanagedRun() this.ReleaseLock(MAController.GlobalSynchronizationStepLock, nameof(MAController.GlobalSynchronizationStepLock)); this.HasSyncLock = false; } + + if (this.HasExclusiveLock) + { + this.ReleaseLock(MAController.GlobalExclusiveOperationLock, nameof(MAController.GlobalExclusiveOperationLock)); + this.HasExclusiveLock = false; + } + + if (hasRunLock) + { + this.ReleaseLock(MAController.GlobalExclusiveOperationRunningLock, nameof(MAController.GlobalExclusiveOperationRunningLock)); + hasRunLock = false; + } } } else @@ -1434,6 +1476,18 @@ private void Init() } private void TakeLocksAndExecute(ExecutionParameters action) + { + if (RegistrySettings.LockMode == 0) + { + this.TakeLocksAndExecuteOld(action); + } + else + { + this.TakeLocksAndExecuteNew(action); + } + } + + private void TakeLocksAndExecuteOld(ExecutionParameters action) { ConcurrentBag otherLocks = new ConcurrentBag(); bool hasLocalLock = false; @@ -1597,6 +1651,211 @@ private void TakeLocksAndExecute(ExecutionParameters action) } } + private void TakeLocksAndExecuteNew(ExecutionParameters action) + { + ConcurrentBag otherLocks = null; + bool hasLocalLock = false; + Stopwatch totalWaitTimer = new Stopwatch(); + totalWaitTimer.Start(); + Stopwatch opTimer = new Stopwatch(); + bool hasGlobalRuningLock = false; + + try + { + this.WaitOnUnmanagedRun(); + this.ExecutionState = ControllerState.Waiting; + + this.jobCancellationTokenSource = this.CreateJobTokenSource(); + + opTimer.Start(); + + if (action.Exclusive) + { + this.Message = "Waiting to take x-lock"; + this.LogInfo($"Entering exclusive mode for {action.RunProfileName}"); + + // Give any waiting non-exclusive operations a chance to kick off before locking things up completely + if (RegistrySettings.ExclusiveModeDeferralInterval.TotalSeconds > 0) + { + Thread.Sleep(RegistrySettings.ExclusiveModeDeferralInterval); + } + + // Signal all controllers to wait before running their next job + this.WaitAndTakeLockWithSemaphore(MAController.GlobalExclusiveOperationLock, MAController.GlobalExclusiveOperationLockSemaphore, nameof(MAController.GlobalExclusiveOperationLock), this.jobCancellationTokenSource); + this.HasExclusiveLock = true; + } + else + { + this.Message = "Waiting for xr-lock holder to finish"; + this.Wait(MAController.GlobalExclusiveOperationRunningLock, nameof(MAController.GlobalExclusiveOperationRunningLock), this.jobCancellationTokenSource); + + if (RegistrySettings.LockMode != 2) + { + if (MAController.WaitingExclusiveOpID != 0 && action.QueueID > MAController.WaitingExclusiveOpID) + { + this.Message = "Yielding to x-lock holder"; + this.Wait(MAController.GlobalExclusiveOperationLock, nameof(MAController.GlobalExclusiveOperationLock), this.jobCancellationTokenSource); + } + } + } + + opTimer.Stop(); + this.counters.AddWaitTimeExclusive(opTimer.Elapsed); + + otherLocks = this.GetForeignLocks(); + + if (action.Exclusive) + { + // Wait for all MAs to finish their current job + MAController.WaitingExclusiveOpID = Interlocked.Read(ref MAController.CurrentQueueID); + + this.Message = "Yielding to other MAs"; + this.LogInfo($"Waiting for all MAs to complete. Will allow executions up to queue ID {MAController.WaitingExclusiveOpID}"); + this.Wait(MAController.AllMaLocalOperationLocks.Values.Where(t => t != this.localOperationLock).Select(t => t.AvailableWaitHandle).ToArray(), nameof(MAController.AllMaLocalOperationLocks), this.jobCancellationTokenSource); + + this.Message = "Waiting to take xr-lock"; + this.WaitAndTakeLockWithSemaphore(MAController.GlobalExclusiveOperationRunningLock, MAController.GlobalExclusiveOperationRunningLockSemaphore, nameof(MAController.GlobalExclusiveOperationRunningLock), this.jobCancellationTokenSource); + hasGlobalRuningLock = true; + + this.Message = "Waiting for other MAs to finish"; + this.Wait(MAController.AllMaLocalOperationLocks.Values.Where(t => t != this.localOperationLock).Select(t => t.AvailableWaitHandle).ToArray(), nameof(MAController.AllMaLocalOperationLocks), this.jobCancellationTokenSource); + } + + if (this.StepRequiresSyncLock(action.RunProfileName)) + { + this.Message = "Waiting to take s-lock"; + this.LogInfo("Waiting to take sync lock"); + opTimer.Start(); + this.WaitAndTakeLock(MAController.GlobalSynchronizationStepLock, nameof(MAController.GlobalSynchronizationStepLock), this.jobCancellationTokenSource); + opTimer.Stop(); + this.counters.AddWaitTimeSync(opTimer.Elapsed); + this.HasSyncLock = true; + } + + // If another operation in this controller is already running, then wait for it to finish before taking the lock for ourselves + this.Message = "Waiting to take l-lock"; + this.WaitAndTakeLock(this.localOperationLock, nameof(this.localOperationLock), this.jobCancellationTokenSource); + hasLocalLock = true; + + // Grab the staggered execution lock, and hold for x seconds + // This ensures that no MA can start within x seconds of another MA + // to avoid deadlock conditions + this.Message = "Preparing to start management agent"; + bool tookStaggerLock = false; + try + { + this.WaitAndTakeLock(MAController.GlobalStaggeredExecutionLock, nameof(MAController.GlobalStaggeredExecutionLock), this.jobCancellationTokenSource); + tookStaggerLock = true; + this.Wait(RegistrySettings.ExecutionStaggerInterval, nameof(RegistrySettings.ExecutionStaggerInterval), this.jobCancellationTokenSource); + } + finally + { + if (tookStaggerLock) + { + this.ReleaseLock(MAController.GlobalStaggeredExecutionLock, nameof(MAController.GlobalStaggeredExecutionLock)); + } + } + + totalWaitTimer.Stop(); + this.counters.AddWaitTime(totalWaitTimer.Elapsed); + this.LogInfo($"Locks obtained in {totalWaitTimer.Elapsed:hh\\:mm\\:ss}"); + this.Execute(action, this.jobCancellationTokenSource); + } + catch (OperationCanceledException) + { + } + catch (ThresholdExceededException ex) + { + this.LogWarn($"Threshold was exceeded on management agent run profile {this.ExecutingRunProfile}. The controller will be stopped\n{ex.Message}"); + this.SendThresholdExceededMail(ex.RunDetails, ex.Message); + this.Stop(false, false, true); + } + finally + { + this.UpdateExecutionStatus(ControllerState.Idle, null, null); + + if (hasGlobalRuningLock) + { + this.ReleaseLock(MAController.GlobalExclusiveOperationRunningLock, nameof(MAController.GlobalExclusiveOperationRunningLock)); + } + + if (this.HasSyncLock) + { + this.ReleaseLock(MAController.GlobalSynchronizationStepLock, nameof(MAController.GlobalSynchronizationStepLock)); + this.HasSyncLock = false; + } + + if (this.HasExclusiveLock) + { + // Reset the global lock so pending operations can run + MAController.WaitingExclusiveOpID = 0; + this.ReleaseLock(MAController.GlobalExclusiveOperationLock, nameof(MAController.GlobalExclusiveOperationLock)); + this.HasExclusiveLock = false; + } + + if (otherLocks?.Any() ?? false) + { + foreach (SemaphoreSlim e in otherLocks) + { + this.ReleaseLock(e, "foreign localOperationLock"); + } + + this.HasForeignLock = false; + } + + if (hasLocalLock) + { + // Reset the local lock so the next operation can run + this.ReleaseLock(this.localOperationLock, nameof(this.localOperationLock)); + } + } + } + + private ConcurrentBag GetForeignLocks() + { + ConcurrentBag otherLocks = new ConcurrentBag(); + + if (this.Configuration.LockManagementAgents != null) + { + List tasks = new List(); + + foreach (string managementAgent in this.Configuration.LockManagementAgents) + { + Guid? id = Global.FindManagementAgent(managementAgent, Guid.Empty); + + if (id == null) + { + this.LogInfo($"Cannot take lock for management agent {managementAgent} as the management agent cannot be found"); + continue; + } + + if (id == this.ManagementAgentID) + { + this.Trace("Not going to wait on own lock!"); + continue; + } + + tasks.Add(Task.Run(() => + { + Thread.CurrentThread.SetThreadName($"Get localOperationLock on {managementAgent} for {this.ManagementAgentName}"); + SemaphoreSlim h = MAController.AllMaLocalOperationLocks[id.Value]; + this.WaitAndTakeLock(h, $"localOperationLock for {managementAgent}", this.jobCancellationTokenSource); + otherLocks.Add(h); + this.HasForeignLock = true; + }, this.jobCancellationTokenSource.Token)); + } + + if (tasks.Any()) + { + this.Message = $"Waiting to take locks"; + Task.WaitAll(tasks.ToArray(), this.jobCancellationTokenSource.Token); + } + } + + return otherLocks; + } + + private void SetExclusiveMode(ExecutionParameters action) { if (Program.ActiveConfig.Settings.RunMode == RunMode.Exclusive) @@ -1716,6 +1975,11 @@ private void MAController_SyncComplete(object sender, SyncCompleteEventArgs e) return; } + if (this.ControlState != ControlState.Running) + { + return; + } + this.Trace($"Got sync complete message from {e.SendingMAName}"); foreach (PartitionConfiguration c in this.GetPartitionsRequiringExport()) @@ -1812,7 +2076,7 @@ internal void AddPendingActionIfNotQueued(string runProfileName, string source) this.AddPendingActionIfNotQueued(new ExecutionParameters(runProfileName), source); } - internal void AddPendingActionIfNotQueued(ExecutionParameters p, string source, bool runNext = false) + internal void AddPendingActionIfNotQueued(ExecutionParameters p, string source) { try { @@ -1848,7 +2112,7 @@ internal void AddPendingActionIfNotQueued(ExecutionParameters p, string source, if (this.pendingActions.ToArray().Contains(p)) { - if (runNext && this.pendingActions.Count > 1) + if (p.RunImmediate && this.pendingActions.Count > 1) { this.LogInfo($"Moving {p.RunProfileName} to the front of the execution queue"); this.pendingActionList.MoveToFront(p); @@ -1870,9 +2134,11 @@ internal void AddPendingActionIfNotQueued(ExecutionParameters p, string source, // return; //} - this.Trace($"Got queue request for {p.RunProfileName}"); + p.QueueID = Interlocked.Increment(ref MAController.CurrentQueueID); + + this.Trace($"Got queue request for {p.RunProfileName} with id {p.QueueID}"); - if (runNext) + if (p.RunImmediate) { this.pendingActions.Add(p, this.controllerCancellationTokenSource.Token); this.pendingActionList.MoveToFront(p); diff --git a/src/Lithnet.Miiserver.AutoSync/PerfCounters.cs b/src/Lithnet.Miiserver.AutoSync/PerfCounters.cs index fae8f9a..e1e36cb 100644 --- a/src/Lithnet.Miiserver.AutoSync/PerfCounters.cs +++ b/src/Lithnet.Miiserver.AutoSync/PerfCounters.cs @@ -1,15 +1,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Timers; +using NLog; namespace Lithnet.Miiserver.AutoSync { internal class MAControllerPerfCounters { + private static Logger logger = LogManager.GetCurrentClassLogger(); + private Stopwatch activeTimer; private Timer timer; @@ -41,7 +41,7 @@ internal class MAControllerPerfCounters public MAControllerPerfCounters(string maName) { - this.RunCount = MAControllerPerfCounters.CreateCounter("Total run count", maName); + this.RunCount = MAControllerPerfCounters.CreateCounter("Runs/10 min", maName); this.WaitTimeAverageSyncLock = MAControllerPerfCounters.CreateCounter("Wait time average - sync lock", maName); this.WaitTimeAverageExclusiveLock = MAControllerPerfCounters.CreateCounter("Wait time average - exclusive lock", maName); this.WaitTimeAverage = MAControllerPerfCounters.CreateCounter("Wait time average", maName); @@ -57,6 +57,7 @@ public MAControllerPerfCounters(string maName) this.timer = new Timer(); this.timer.Interval = TimeSpan.FromSeconds(5).TotalMilliseconds; this.timer.Elapsed += this.Timer_Elapsed; + this.executionHistory = new List(); } private void Timer_Elapsed(object sender, ElapsedEventArgs e) @@ -67,6 +68,7 @@ private void Timer_Elapsed(object sender, ElapsedEventArgs e) public void Stop() { this.timer?.Stop(); + this.activeTimer?.Stop(); this.ResetValues(); } @@ -82,6 +84,7 @@ private void ResetValues() this.CurrentQueueLength.RawValue = 0; this.ExecutionTimeTotal.RawValue = 0; this.ExecutionTimeAverage.RawValue = 0; + this.IdleTimePercent.RawValue = 0; this.WaitTime.RawValue = 0; this.WaitTimeExclusiveLock.RawValue = 0; this.WaitTimeSyncLock.RawValue = 0; @@ -89,6 +92,21 @@ private void ResetValues() this.WaitTimeAverageExclusiveLock.RawValue = 0; this.WaitTimeAverageSyncLock.RawValue = 0; this.RunCount.RawValue = 0; + + this.executionTimeCounts = 0; + this.executionTimeTotal = new TimeSpan(); + + this.waitTimeCounts = 0; + this.waitTimeTotal = new TimeSpan(); + + this.waitTimeSyncCounts = 0; + this.waitTimeSync = new TimeSpan(); + + this.waitTimeExclusiveCounts = 0; + this.waitTimeExclusive = new TimeSpan(); + + this.executionHistory.Clear(); + } private static PerformanceCounter CreateCounter(string name, string maName) @@ -105,13 +123,16 @@ private static PerformanceCounter CreateCounter(string name, string maName) } private int executionTimeCounts; - private TimeSpan executionTimeTotal; + private List executionHistory; + public void AddExecutionTime(TimeSpan value) { this.executionTimeTotal = this.executionTimeTotal.Add(value); this.executionTimeCounts++; + this.executionHistory.Add(DateTime.Now); + this.UpdateCounters(); } @@ -147,38 +168,49 @@ public void AddWaitTimeExclusive(TimeSpan value) private void UpdateCounters() { - double elapsed = this.activeTimer.Elapsed.TotalSeconds; - - if (elapsed <= 0) + try { - return; + double elapsed = this.activeTimer.Elapsed.TotalSeconds; + + if (elapsed <= 0) + { + return; + } + + if (this.waitTimeExclusiveCounts > 0) + { + this.WaitTimeAverageExclusiveLock.RawValue = Convert.ToInt64(this.waitTimeExclusive.TotalSeconds / this.waitTimeExclusiveCounts); + this.WaitTimeExclusiveLock.RawValue = (long) (this.waitTimeExclusive.TotalSeconds / this.activeTimer.Elapsed.TotalSeconds * 100); + } + + if (this.waitTimeSyncCounts > 0) + { + this.WaitTimeAverageSyncLock.RawValue = Convert.ToInt64(this.waitTimeSync.TotalSeconds / this.waitTimeSyncCounts); + this.WaitTimeSyncLock.RawValue = (long) (this.waitTimeSync.TotalSeconds / this.activeTimer.Elapsed.TotalSeconds * 100); + } + + if (this.waitTimeCounts > 0) + { + this.WaitTimeAverage.RawValue = Convert.ToInt64(this.waitTimeTotal.TotalSeconds / this.waitTimeCounts); + this.WaitTime.RawValue = (long) (this.waitTimeTotal.TotalSeconds / this.activeTimer.Elapsed.TotalSeconds * 100); + } + + if (this.executionTimeCounts > 0) + { + this.ExecutionTimeAverage.RawValue = Convert.ToInt64(this.executionTimeTotal.TotalSeconds / this.executionTimeCounts); + this.ExecutionTimeTotal.RawValue = (long) (this.executionTimeTotal.TotalSeconds / this.activeTimer.Elapsed.TotalSeconds * 100); + } + + this.IdleTimePercent.RawValue = (long) (((this.activeTimer.Elapsed.TotalSeconds - this.executionTimeTotal.TotalSeconds - this.waitTimeTotal.TotalSeconds) / this.activeTimer.Elapsed.TotalSeconds) * 100); + + this.executionHistory.RemoveAll(t => t < DateTime.Now.AddMinutes(-10)); + + this.RunCount.RawValue = this.executionHistory.Count; } - - if (this.waitTimeExclusiveCounts > 0) - { - this.WaitTimeAverageExclusiveLock.RawValue = Convert.ToInt64(this.waitTimeExclusive.TotalSeconds / this.waitTimeExclusiveCounts); - this.WaitTimeExclusiveLock.RawValue = (long) (this.waitTimeExclusive.TotalSeconds / this.activeTimer.Elapsed.TotalSeconds * 100); - } - - if (this.waitTimeSyncCounts > 0) + catch (Exception ex) { - this.WaitTimeAverageSyncLock.RawValue = Convert.ToInt64(this.waitTimeSync.TotalSeconds / this.waitTimeSyncCounts); - this.WaitTimeSyncLock.RawValue = (long) (this.waitTimeSync.TotalSeconds / this.activeTimer.Elapsed.TotalSeconds * 100); + logger.Warn(ex, "Performance counter update failed"); } - - if (this.waitTimeCounts > 0) - { - this.WaitTimeAverage.RawValue = Convert.ToInt64(this.waitTimeTotal.TotalSeconds / this.waitTimeCounts); - this.WaitTime.RawValue = (long) (this.waitTimeTotal.TotalSeconds / this.activeTimer.Elapsed.TotalSeconds * 100); - } - - if (this.executionTimeCounts > 0) - { - this.ExecutionTimeAverage.RawValue = Convert.ToInt64(this.executionTimeTotal.TotalSeconds / this.executionTimeCounts); - this.ExecutionTimeTotal.RawValue = (long) (this.executionTimeTotal.TotalSeconds / this.activeTimer.Elapsed.TotalSeconds * 100); - } - - this.IdleTimePercent.RawValue = (long)(((this.activeTimer.Elapsed.TotalSeconds - this.executionTimeTotal.TotalSeconds - this.waitTimeTotal.TotalSeconds) / this.activeTimer.Elapsed.TotalSeconds) * 100); } } } \ No newline at end of file diff --git a/src/Lithnet.Miiserver.AutoSync/Triggers/ActiveDirectoryChangeTrigger.cs b/src/Lithnet.Miiserver.AutoSync/Triggers/ActiveDirectoryChangeTrigger.cs index 4654c4a..2a01c5b 100644 --- a/src/Lithnet.Miiserver.AutoSync/Triggers/ActiveDirectoryChangeTrigger.cs +++ b/src/Lithnet.Miiserver.AutoSync/Triggers/ActiveDirectoryChangeTrigger.cs @@ -83,9 +83,6 @@ public NetworkCredential GetCredentialPackage() [DataMember(Name = "last-logon-offset")] public TimeSpan LastLogonTimestampOffset { get; set; } - [DataMember(Name = "disabled")] - public bool Disabled { get; set; } - [DataMember(Name = "use-explicit-credentials")] public bool UseExplicitCredentials { get; set; } @@ -301,7 +298,7 @@ public override void Stop() public override string Type => TypeDescription; - public override string Description => $"{this.BaseDN}"; + public override string Description => $"{this.DisabledText}{this.BaseDN}"; public override string ToString() { diff --git a/src/Lithnet.Miiserver.AutoSync/Triggers/FimServicePendingImportTrigger.cs b/src/Lithnet.Miiserver.AutoSync/Triggers/FimServicePendingImportTrigger.cs index 3b37303..f128aa9 100644 --- a/src/Lithnet.Miiserver.AutoSync/Triggers/FimServicePendingImportTrigger.cs +++ b/src/Lithnet.Miiserver.AutoSync/Triggers/FimServicePendingImportTrigger.cs @@ -75,6 +75,12 @@ private void CheckTimer_Elapsed(object sender, ElapsedEventArgs e) public override void Start(string managementAgentName) { + if (this.Disabled) + { + this.Log("Trigger disabled"); + return; + } + this.ManagementAgentName = managementAgentName; this.checkTimer = new Timer @@ -184,7 +190,7 @@ public static void CreateMpr(string hostname, NetworkCredential creds, string ac public override string Type => TypeDescription; - public override string Description => $"{this.HostName}"; + public override string Description => $"{this.DisabledText}{this.HostName}"; private static string GetFimServiceHostName(ManagementAgent ma) { diff --git a/src/Lithnet.Miiserver.AutoSync/Triggers/IMAExecutionTrigger.cs b/src/Lithnet.Miiserver.AutoSync/Triggers/IMAExecutionTrigger.cs index 5b9dc0f..47c9004 100644 --- a/src/Lithnet.Miiserver.AutoSync/Triggers/IMAExecutionTrigger.cs +++ b/src/Lithnet.Miiserver.AutoSync/Triggers/IMAExecutionTrigger.cs @@ -8,6 +8,8 @@ namespace Lithnet.Miiserver.AutoSync public interface IMAExecutionTrigger { + bool Disabled { get; } + string DisplayName { get; } string Type { get; } diff --git a/src/Lithnet.Miiserver.AutoSync/Triggers/IntervalExecutionTrigger.cs b/src/Lithnet.Miiserver.AutoSync/Triggers/IntervalExecutionTrigger.cs index 57d704c..d258b11 100644 --- a/src/Lithnet.Miiserver.AutoSync/Triggers/IntervalExecutionTrigger.cs +++ b/src/Lithnet.Miiserver.AutoSync/Triggers/IntervalExecutionTrigger.cs @@ -24,14 +24,27 @@ public class IntervalExecutionTrigger : MAExecutionTrigger [DataMember(Name = "exclusive")] public bool Exclusive { get; set; } + [DataMember(Name = "run-immediate")] + public bool RunImmediate { get; set; } + private void CheckTimer_Elapsed(object sender, ElapsedEventArgs e) { this.Log($"Timer elapsed. Next event at {DateTime.Now.Add(this.Interval)}"); - this.Fire(this.RunProfileName, this.Exclusive); + ExecutionParameters p = new ExecutionParameters(); + p.RunProfileName = this.RunProfileName; + p.Exclusive = this.Exclusive; + p.RunImmediate = this.RunImmediate; + this.Fire(p); } public override void Start(string managementAgentName) { + if (this.Disabled) + { + this.Log("Trigger disabled"); + return; + } + this.ManagementAgentName = managementAgentName; if (this.RunProfileName == null) @@ -70,7 +83,7 @@ public override void Stop() public override string Type => TypeDescription; - public override string Description => $"{this.RunProfileName} every {this.Interval}"; + public override string Description => $"{this.DisabledText}{this.RunProfileName} every {this.Interval}"; public override string ToString() { diff --git a/src/Lithnet.Miiserver.AutoSync/Triggers/MAExecutionTrigger.cs b/src/Lithnet.Miiserver.AutoSync/Triggers/MAExecutionTrigger.cs index e5c7113..e48682e 100644 --- a/src/Lithnet.Miiserver.AutoSync/Triggers/MAExecutionTrigger.cs +++ b/src/Lithnet.Miiserver.AutoSync/Triggers/MAExecutionTrigger.cs @@ -17,6 +17,24 @@ public abstract class MAExecutionTrigger : IMAExecutionTrigger public static IList SingleInstanceTriggers = new List() { typeof(FimServicePendingImportTrigger) }; + protected string DisabledText + { + get + { + if (this.Disabled) + { + return "(Disabled) "; + } + else + { + return string.Empty; + } + } + } + + [DataMember(Name = "disabled")] + public bool Disabled { get; set; } + public abstract string DisplayName { get; } public abstract string Type { get; } diff --git a/src/Lithnet.Miiserver.AutoSync/Triggers/PowerShellExecutionTrigger.cs b/src/Lithnet.Miiserver.AutoSync/Triggers/PowerShellExecutionTrigger.cs index a16d1fa..d6394c4 100644 --- a/src/Lithnet.Miiserver.AutoSync/Triggers/PowerShellExecutionTrigger.cs +++ b/src/Lithnet.Miiserver.AutoSync/Triggers/PowerShellExecutionTrigger.cs @@ -49,13 +49,19 @@ public PSCredential GetCredentialPackage() public override string Type => TypeDescription; - public override string Description => $"{System.IO.Path.GetFileName(this.ScriptPath)}"; + public override string Description => $"{this.DisabledText}{System.IO.Path.GetFileName(this.ScriptPath)}"; [DataMember(Name = "interval")] public TimeSpan Interval { get; set; } public override void Start(string managementAgentName) { + if (this.Disabled) + { + this.Log("Trigger disabled"); + return; + } + this.ManagementAgentName = managementAgentName; if (!System.IO.File.Exists(this.ScriptPath)) diff --git a/src/Lithnet.Miiserver.AutoSync/Triggers/ScheduledExecutionTrigger.cs b/src/Lithnet.Miiserver.AutoSync/Triggers/ScheduledExecutionTrigger.cs index bf4218e..84a93a8 100644 --- a/src/Lithnet.Miiserver.AutoSync/Triggers/ScheduledExecutionTrigger.cs +++ b/src/Lithnet.Miiserver.AutoSync/Triggers/ScheduledExecutionTrigger.cs @@ -27,6 +27,9 @@ public class ScheduledExecutionTrigger : MAExecutionTrigger [DataMember(Name = "exclusive")] public bool Exclusive { get; set; } + [DataMember(Name = "run-immediate")] + public bool RunImmediate { get; set; } + private double RemainingMilliseconds { get; set; } private void SetRemainingMilliseconds() @@ -55,12 +58,22 @@ private void SetRemainingMilliseconds() private void CheckTimer_Elapsed(object sender, ElapsedEventArgs e) { - this.Fire(this.RunProfileName, this.Exclusive); + ExecutionParameters p= new ExecutionParameters(); + p.RunProfileName = this.RunProfileName; + p.Exclusive = this.Exclusive; + p.RunImmediate = this.RunImmediate; + this.Fire(p); this.ResetTimer(); } public override void Start(string managementAgentName) { + if (this.Disabled) + { + this.Log("Trigger disabled"); + return; + } + this.ManagementAgentName = managementAgentName; if (this.RunProfileName == null) @@ -102,7 +115,7 @@ public override void Stop() public override string Type => TypeDescription; - public override string Description => $"{this.RunProfileName} every {this.Interval} starting from {this.StartDateTime:F}"; + public override string Description => $"{this.DisabledText}{this.RunProfileName} every {this.Interval} starting from {this.StartDateTime:F}"; public override string ToString() { diff --git a/src/Lithnet.Miiserver.Autosync.UI/ViewModels/ActiveDirectoryChangeTriggerViewModel.cs b/src/Lithnet.Miiserver.Autosync.UI/ViewModels/ActiveDirectoryChangeTriggerViewModel.cs index a8deb0c..3075413 100644 --- a/src/Lithnet.Miiserver.Autosync.UI/ViewModels/ActiveDirectoryChangeTriggerViewModel.cs +++ b/src/Lithnet.Miiserver.Autosync.UI/ViewModels/ActiveDirectoryChangeTriggerViewModel.cs @@ -98,6 +98,7 @@ public string Password public string Description => this.Model.Description; + [AlsoNotifyFor("Description")] public bool Disabled { get => this.typedModel.Disabled; diff --git a/src/Lithnet.Miiserver.Autosync.UI/ViewModels/FimServicePendingImportTriggerViewModel.cs b/src/Lithnet.Miiserver.Autosync.UI/ViewModels/FimServicePendingImportTriggerViewModel.cs index f081c71..d14b319 100644 --- a/src/Lithnet.Miiserver.Autosync.UI/ViewModels/FimServicePendingImportTriggerViewModel.cs +++ b/src/Lithnet.Miiserver.Autosync.UI/ViewModels/FimServicePendingImportTriggerViewModel.cs @@ -21,6 +21,7 @@ public FimServicePendingImportTriggerViewModel(FimServicePendingImportTrigger mo this.typedModel = model; this.AddIsDirtyProperty(nameof(this.HostName)); this.AddIsDirtyProperty(nameof(this.Interval)); + this.AddIsDirtyProperty(nameof(this.Disabled)); this.Commands.AddItem("CreateMPR", x => this.CreateMpr()); } @@ -29,6 +30,13 @@ public FimServicePendingImportTriggerViewModel(FimServicePendingImportTrigger mo public string Description => this.Model.Description; + [AlsoNotifyFor("Description")] + public bool Disabled + { + get => this.typedModel.Disabled; + set => this.typedModel.Disabled = value; + } + [AlsoNotifyFor("Description")] public string HostName { diff --git a/src/Lithnet.Miiserver.Autosync.UI/ViewModels/IntervalExecutionTriggerViewModel.cs b/src/Lithnet.Miiserver.Autosync.UI/ViewModels/IntervalExecutionTriggerViewModel.cs index 685f8f9..e943624 100644 --- a/src/Lithnet.Miiserver.Autosync.UI/ViewModels/IntervalExecutionTriggerViewModel.cs +++ b/src/Lithnet.Miiserver.Autosync.UI/ViewModels/IntervalExecutionTriggerViewModel.cs @@ -19,6 +19,8 @@ public IntervalExecutionTriggerViewModel(IntervalExecutionTrigger model, MAContr this.AddIsDirtyProperty(nameof(this.RunProfileName)); this.AddIsDirtyProperty(nameof(this.Interval)); this.AddIsDirtyProperty(nameof(this.Exclusive)); + this.AddIsDirtyProperty(nameof(this.RunImmediate)); + this.AddIsDirtyProperty(nameof(this.Disabled)); } public string Type => this.Model.Type; @@ -31,6 +33,18 @@ public bool Exclusive set => this.typedModel.Exclusive = value; } + [AlsoNotifyFor("Description")] + public bool Disabled + { + get => this.typedModel.Disabled; + set => this.typedModel.Disabled = value; + } + public bool RunImmediate + { + get => this.typedModel.RunImmediate; + set => this.typedModel.RunImmediate = value; + } + [AlsoNotifyFor("Description")] public string RunProfileName { diff --git a/src/Lithnet.Miiserver.Autosync.UI/ViewModels/PowerShellExecutionTriggerViewModel.cs b/src/Lithnet.Miiserver.Autosync.UI/ViewModels/PowerShellExecutionTriggerViewModel.cs index 8ec9463..8e8156a 100644 --- a/src/Lithnet.Miiserver.Autosync.UI/ViewModels/PowerShellExecutionTriggerViewModel.cs +++ b/src/Lithnet.Miiserver.Autosync.UI/ViewModels/PowerShellExecutionTriggerViewModel.cs @@ -27,6 +27,14 @@ public PowerShellExecutionTriggerViewModel(PowerShellExecutionTrigger model) this.AddIsDirtyProperty(nameof(this.ExceptionBehaviour)); this.AddIsDirtyProperty(nameof(this.Username)); this.AddIsDirtyProperty(nameof(this.Password)); + this.AddIsDirtyProperty(nameof(this.Disabled)); + } + + [AlsoNotifyFor("Description")] + public bool Disabled + { + get => this.typedModel.Disabled; + set => this.typedModel.Disabled = value; } [AlsoNotifyFor("Description")] diff --git a/src/Lithnet.Miiserver.Autosync.UI/ViewModels/ScheduledExecutionTriggerViewModel.cs b/src/Lithnet.Miiserver.Autosync.UI/ViewModels/ScheduledExecutionTriggerViewModel.cs index 4638c4c..679cc34 100644 --- a/src/Lithnet.Miiserver.Autosync.UI/ViewModels/ScheduledExecutionTriggerViewModel.cs +++ b/src/Lithnet.Miiserver.Autosync.UI/ViewModels/ScheduledExecutionTriggerViewModel.cs @@ -21,18 +21,33 @@ public ScheduledExecutionTriggerViewModel(ScheduledExecutionTrigger model, MACon this.AddIsDirtyProperty(nameof(this.Interval)); this.AddIsDirtyProperty(nameof(this.StartDateTime)); this.AddIsDirtyProperty(nameof(this.Exclusive)); + this.AddIsDirtyProperty(nameof(this.RunImmediate)); + this.AddIsDirtyProperty(nameof(this.Disabled)); } public string Type => this.Model.Type; public string Description => this.Model.Description; + [AlsoNotifyFor("Description")] + public bool Disabled + { + get => this.typedModel.Disabled; + set => this.typedModel.Disabled = value; + } + public bool Exclusive { get => this.typedModel.Exclusive; set => this.typedModel.Exclusive = value; } + public bool RunImmediate + { + get => this.typedModel.RunImmediate; + set => this.typedModel.RunImmediate = value; + } + [AlsoNotifyFor("Description")] public string RunProfileName { diff --git a/src/Lithnet.Miiserver.Autosync.UI/Views/ActiveDirectoryChangeTriggerView.xaml b/src/Lithnet.Miiserver.Autosync.UI/Views/ActiveDirectoryChangeTriggerView.xaml index c447d36..3ce8f0c 100644 --- a/src/Lithnet.Miiserver.Autosync.UI/Views/ActiveDirectoryChangeTriggerView.xaml +++ b/src/Lithnet.Miiserver.Autosync.UI/Views/ActiveDirectoryChangeTriggerView.xaml @@ -24,36 +24,39 @@ + - + + - - - - - - - - - - - - - - - + + diff --git a/src/Lithnet.Miiserver.Autosync.UI/Views/FimServicePendingImportTriggerView.xaml b/src/Lithnet.Miiserver.Autosync.UI/Views/FimServicePendingImportTriggerView.xaml index de7cd89..d7ed90e 100644 --- a/src/Lithnet.Miiserver.Autosync.UI/Views/FimServicePendingImportTriggerView.xaml +++ b/src/Lithnet.Miiserver.Autosync.UI/Views/FimServicePendingImportTriggerView.xaml @@ -18,31 +18,34 @@ + -