< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.ScheduledTasks.ScheduledTaskWorker
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
Line coverage
64%
Covered lines: 124
Uncovered lines: 67
Coverable lines: 191
Total lines: 679
Line coverage: 64.9%
Branch coverage
67%
Covered branches: 42
Total branches: 62
Branch coverage: 67.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_LastExecutionResult()62.5%17.71846.66%
set_LastExecutionResult(...)100%11100%
get_Name()100%11100%
get_Description()100%210%
get_Category()100%210%
get_State()75%44100%
get_InternalTriggers()100%11100%
set_InternalTriggers(...)0%620%
get_Triggers()100%210%
set_Triggers(...)100%210%
get_Id()100%22100%
InitTriggerEvents()100%11100%
ReloadTriggerEvents()100%210%
ReloadTriggerEvents(...)100%22100%
OnProgressChanged(...)100%22100%
Cancel()0%620%
CancelIfRunning()50%2.5250%
GetScheduledTasksConfigurationDirectory()100%11100%
GetScheduledTasksDataDirectory()100%11100%
GetHistoryFilePath()100%11100%
GetConfigurationFilePath()100%11100%
LoadTriggers()100%11100%
LoadTriggerSettings()75%4.59466.66%
GetDefaultTriggers()100%1.55118.18%
SaveTriggers(...)100%210%
OnTaskCompleted(...)50%2.01288.23%
Dispose()100%11100%
Dispose(...)91.66%16.011269.69%
GetTrigger(...)50%30.161661.9%
DisposeTriggers()100%44100%

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs

#LineLine coverage
 1#nullable disable
 2
 3using System;
 4using System.Collections.Generic;
 5using System.Globalization;
 6using System.IO;
 7using System.Linq;
 8using System.Text.Json;
 9using System.Threading;
 10using System.Threading.Tasks;
 11using Emby.Server.Implementations.ScheduledTasks.Triggers;
 12using Jellyfin.Data.Events;
 13using Jellyfin.Extensions.Json;
 14using MediaBrowser.Common.Configuration;
 15using MediaBrowser.Common.Extensions;
 16using MediaBrowser.Model.Tasks;
 17using Microsoft.Extensions.Logging;
 18
 19namespace Emby.Server.Implementations.ScheduledTasks
 20{
 21    /// <summary>
 22    /// Class ScheduledTaskWorker.
 23    /// </summary>
 24    public class ScheduledTaskWorker : IScheduledTaskWorker
 25    {
 41826        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
 27        private readonly IApplicationPaths _applicationPaths;
 28        private readonly ILogger _logger;
 29        private readonly ITaskManager _taskManager;
 41830        private readonly object _lastExecutionResultSyncLock = new();
 31        private bool _readFromFile;
 32        private TaskResult _lastExecutionResult;
 33        private Task _currentTask;
 34        private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers;
 35        private string _id;
 36
 37        /// <summary>
 38        /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
 39        /// </summary>
 40        /// <param name="scheduledTask">The scheduled task.</param>
 41        /// <param name="applicationPaths">The application paths.</param>
 42        /// <param name="taskManager">The task manager.</param>
 43        /// <param name="logger">The logger.</param>
 44        /// <exception cref="ArgumentNullException">
 45        /// scheduledTask
 46        /// or
 47        /// applicationPaths
 48        /// or
 49        /// taskManager
 50        /// or
 51        /// jsonSerializer
 52        /// or
 53        /// logger.
 54        /// </exception>
 55        public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskMa
 56        {
 41857            ArgumentNullException.ThrowIfNull(scheduledTask);
 41858            ArgumentNullException.ThrowIfNull(applicationPaths);
 41859            ArgumentNullException.ThrowIfNull(taskManager);
 41860            ArgumentNullException.ThrowIfNull(logger);
 61
 41862            ScheduledTask = scheduledTask;
 41863            _applicationPaths = applicationPaths;
 41864            _taskManager = taskManager;
 41865            _logger = logger;
 66
 41867            InitTriggerEvents();
 41868        }
 69
 70        /// <inheritdoc />
 71        public event EventHandler<GenericEventArgs<double>> TaskProgress;
 72
 73        /// <inheritdoc />
 74        public IScheduledTask ScheduledTask { get; private set; }
 75
 76        /// <inheritdoc />
 77        public TaskResult LastExecutionResult
 78        {
 79            get
 80            {
 39981                var path = GetHistoryFilePath();
 82
 39983                lock (_lastExecutionResultSyncLock)
 84                {
 39985                    if (_lastExecutionResult is null && !_readFromFile)
 86                    {
 35287                        if (File.Exists(path))
 88                        {
 089                            var bytes = File.ReadAllBytes(path);
 090                            if (bytes.Length > 0)
 91                            {
 92                                try
 93                                {
 094                                    _lastExecutionResult = JsonSerializer.Deserialize<TaskResult>(bytes, _jsonOptions);
 095                                }
 096                                catch (JsonException ex)
 97                                {
 098                                    _logger.LogError(ex, "Error deserializing {File}", path);
 099                                }
 100                            }
 101                            else
 102                            {
 0103                                _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.
 104                            }
 105                        }
 106
 352107                        _readFromFile = true;
 108                    }
 399109                }
 110
 399111                return _lastExecutionResult;
 112            }
 113
 114            private set
 115            {
 24116                _lastExecutionResult = value;
 117
 24118                var path = GetHistoryFilePath();
 24119                Directory.CreateDirectory(Path.GetDirectoryName(path));
 120
 24121                lock (_lastExecutionResultSyncLock)
 122                {
 24123                    using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.No
 24124                    using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream);
 24125                    JsonSerializer.Serialize(jsonStream, value, _jsonOptions);
 126                }
 24127            }
 128        }
 129
 130        /// <inheritdoc />
 477131        public string Name => ScheduledTask.Name;
 132
 133        /// <inheritdoc />
 0134        public string Description => ScheduledTask.Description;
 135
 136        /// <inheritdoc />
 0137        public string Category => ScheduledTask.Category;
 138
 139        /// <summary>
 140        /// Gets or sets the current cancellation token.
 141        /// </summary>
 142        /// <value>The current cancellation token source.</value>
 143        private CancellationTokenSource CurrentCancellationTokenSource { get; set; }
 144
 145        /// <summary>
 146        /// Gets or sets the current execution start time.
 147        /// </summary>
 148        /// <value>The current execution start time.</value>
 149        private DateTime CurrentExecutionStartTime { get; set; }
 150
 151        /// <inheritdoc />
 152        public TaskState State
 153        {
 154            get
 155            {
 461156                if (CurrentCancellationTokenSource is not null)
 157                {
 1158                    return CurrentCancellationTokenSource.IsCancellationRequested
 1159                               ? TaskState.Cancelling
 1160                               : TaskState.Running;
 161                }
 162
 460163                return TaskState.Idle;
 164            }
 165        }
 166
 167        /// <inheritdoc />
 168        public double? CurrentProgress { get; private set; }
 169
 170        /// <summary>
 171        /// Gets or sets the triggers that define when the task will run.
 172        /// </summary>
 173        /// <value>The triggers.</value>
 174        private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers
 175        {
 836176            get => _triggers;
 177            set
 178            {
 0179                ArgumentNullException.ThrowIfNull(value);
 180
 181                // Cleanup current triggers
 0182                if (_triggers is not null)
 183                {
 0184                    DisposeTriggers();
 185                }
 186
 0187                _triggers = value.ToArray();
 188
 0189                ReloadTriggerEvents(false);
 0190            }
 191        }
 192
 193        /// <inheritdoc />
 194        public IReadOnlyList<TaskTriggerInfo> Triggers
 195        {
 196            get
 197            {
 0198                return Array.ConvertAll(InternalTriggers, i => i.Item1);
 199            }
 200
 201            set
 202            {
 0203                ArgumentNullException.ThrowIfNull(value);
 204
 205                // This null check is not great, but is needed to handle bad user input, or user mucking with the config
 0206                var triggerList = value.Where(i => i is not null).ToArray();
 207
 0208                SaveTriggers(triggerList);
 209
 0210                InternalTriggers = Array.ConvertAll(triggerList, i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTri
 0211            }
 212        }
 213
 214        /// <inheritdoc />
 215        public string Id
 216        {
 217            get
 218            {
 865219                return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture);
 220            }
 221        }
 222
 223        private void InitTriggerEvents()
 224        {
 418225            _triggers = LoadTriggers();
 418226            ReloadTriggerEvents(true);
 418227        }
 228
 229        /// <inheritdoc />
 230        public void ReloadTriggerEvents()
 231        {
 0232            ReloadTriggerEvents(false);
 0233        }
 234
 235        /// <summary>
 236        /// Reloads the trigger events.
 237        /// </summary>
 238        /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
 239        private void ReloadTriggerEvents(bool isApplicationStartup)
 240        {
 1628241            foreach (var triggerInfo in InternalTriggers)
 242            {
 396243                var trigger = triggerInfo.Item2;
 244
 396245                trigger.Stop();
 246
 396247                trigger.Triggered -= OnTriggerTriggered;
 396248                trigger.Triggered += OnTriggerTriggered;
 396249                trigger.Start(LastExecutionResult, _logger, Name, isApplicationStartup);
 250            }
 418251        }
 252
 253        /// <summary>
 254        /// Handles the Triggered event of the trigger control.
 255        /// </summary>
 256        /// <param name="sender">The source of the event.</param>
 257        /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
 258        private async void OnTriggerTriggered(object sender, EventArgs e)
 259        {
 260            var trigger = (ITaskTrigger)sender;
 261
 262            if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled)
 263            {
 264                return;
 265            }
 266
 267            _logger.LogDebug("{0} fired for task: {1}", trigger.GetType().Name, Name);
 268
 269            trigger.Stop();
 270
 271            _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions);
 272
 273            await Task.Delay(1000).ConfigureAwait(false);
 274
 275            trigger.Start(LastExecutionResult, _logger, Name, false);
 276        }
 277
 278        /// <summary>
 279        /// Executes the task.
 280        /// </summary>
 281        /// <param name="options">Task options.</param>
 282        /// <returns>Task.</returns>
 283        /// <exception cref="InvalidOperationException">Cannot execute a Task that is already running.</exception>
 284        public async Task Execute(TaskOptions options)
 285        {
 286            var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false));
 287
 288            _currentTask = task;
 289
 290            try
 291            {
 292                await task.ConfigureAwait(false);
 293            }
 294            finally
 295            {
 296                _currentTask = null;
 297                GC.Collect();
 298            }
 299        }
 300
 301        private async Task ExecuteInternal(TaskOptions options)
 302        {
 303            // Cancel the current execution, if any
 304            if (CurrentCancellationTokenSource is not null)
 305            {
 306                throw new InvalidOperationException("Cannot execute a Task that is already running");
 307            }
 308
 309            var progress = new Progress<double>();
 310
 311            CurrentCancellationTokenSource = new CancellationTokenSource();
 312
 313            _logger.LogDebug("Executing {0}", Name);
 314
 315            ((TaskManager)_taskManager).OnTaskExecuting(this);
 316
 317            progress.ProgressChanged += OnProgressChanged;
 318
 319            TaskCompletionStatus status;
 320            CurrentExecutionStartTime = DateTime.UtcNow;
 321
 322            Exception failureException = null;
 323
 324            try
 325            {
 326                if (options is not null && options.MaxRuntimeTicks.HasValue)
 327                {
 328                    CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value));
 329                }
 330
 331                await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false);
 332
 333                status = TaskCompletionStatus.Completed;
 334            }
 335            catch (OperationCanceledException)
 336            {
 337                status = TaskCompletionStatus.Cancelled;
 338            }
 339            catch (Exception ex)
 340            {
 341                _logger.LogError(ex, "Error executing Scheduled Task");
 342
 343                failureException = ex;
 344
 345                status = TaskCompletionStatus.Failed;
 346            }
 347
 348            var startTime = CurrentExecutionStartTime;
 349            var endTime = DateTime.UtcNow;
 350
 351            progress.ProgressChanged -= OnProgressChanged;
 352            CurrentCancellationTokenSource.Dispose();
 353            CurrentCancellationTokenSource = null;
 354            CurrentProgress = null;
 355
 356            OnTaskCompleted(startTime, endTime, status, failureException);
 357        }
 358
 359        /// <summary>
 360        /// Progress_s the progress changed.
 361        /// </summary>
 362        /// <param name="sender">The sender.</param>
 363        /// <param name="e">The e.</param>
 364        private void OnProgressChanged(object sender, double e)
 365        {
 285366            e = Math.Min(e, 100);
 367
 285368            CurrentProgress = e;
 369
 285370            TaskProgress?.Invoke(this, new GenericEventArgs<double>(e));
 284371        }
 372
 373        /// <summary>
 374        /// Stops the task if it is currently executing.
 375        /// </summary>
 376        /// <exception cref="InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</excepti
 377        public void Cancel()
 378        {
 0379            if (State != TaskState.Running)
 380            {
 0381                throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state.");
 382            }
 383
 0384            CancelIfRunning();
 0385        }
 386
 387        /// <summary>
 388        /// Cancels if running.
 389        /// </summary>
 390        public void CancelIfRunning()
 391        {
 20392            if (State == TaskState.Running)
 393            {
 0394                _logger.LogInformation("Attempting to cancel Scheduled Task {0}", Name);
 0395                CurrentCancellationTokenSource.Cancel();
 396            }
 20397        }
 398
 399        /// <summary>
 400        /// Gets the scheduled tasks configuration directory.
 401        /// </summary>
 402        /// <returns>System.String.</returns>
 403        private string GetScheduledTasksConfigurationDirectory()
 404        {
 418405            return Path.Combine(_applicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
 406        }
 407
 408        /// <summary>
 409        /// Gets the scheduled tasks data directory.
 410        /// </summary>
 411        /// <returns>System.String.</returns>
 412        private string GetScheduledTasksDataDirectory()
 413        {
 423414            return Path.Combine(_applicationPaths.DataPath, "ScheduledTasks");
 415        }
 416
 417        /// <summary>
 418        /// Gets the history file path.
 419        /// </summary>
 420        /// <value>The history file path.</value>
 421        private string GetHistoryFilePath()
 422        {
 423423            return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js");
 424        }
 425
 426        /// <summary>
 427        /// Gets the configuration file path.
 428        /// </summary>
 429        /// <returns>System.String.</returns>
 430        private string GetConfigurationFilePath()
 431        {
 418432            return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js");
 433        }
 434
 435        /// <summary>
 436        /// Loads the triggers.
 437        /// </summary>
 438        /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
 439        private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers()
 440        {
 441            // This null check is not great, but is needed to handle bad user input, or user mucking with the config fil
 418442            var settings = LoadTriggerSettings().Where(i => i is not null);
 443
 418444            return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
 445        }
 446
 447        private TaskTriggerInfo[] LoadTriggerSettings()
 448        {
 418449            string path = GetConfigurationFilePath();
 418450            TaskTriggerInfo[] list = null;
 418451            if (File.Exists(path))
 452            {
 0453                var bytes = File.ReadAllBytes(path);
 0454                list = JsonSerializer.Deserialize<TaskTriggerInfo[]>(bytes, _jsonOptions);
 455            }
 456
 457            // Return defaults if file doesn't exist.
 418458            return list ?? GetDefaultTriggers();
 459        }
 460
 461        private TaskTriggerInfo[] GetDefaultTriggers()
 462        {
 463            try
 464            {
 418465                return ScheduledTask.GetDefaultTriggers().ToArray();
 466            }
 0467            catch
 468            {
 0469                return
 0470                [
 0471                    new()
 0472                    {
 0473                        IntervalTicks = TimeSpan.FromDays(1).Ticks,
 0474                        Type = TaskTriggerInfo.TriggerInterval
 0475                    }
 0476                ];
 477            }
 418478        }
 479
 480        /// <summary>
 481        /// Saves the triggers.
 482        /// </summary>
 483        /// <param name="triggers">The triggers.</param>
 484        private void SaveTriggers(TaskTriggerInfo[] triggers)
 485        {
 0486            var path = GetConfigurationFilePath();
 487
 0488            Directory.CreateDirectory(Path.GetDirectoryName(path));
 0489            using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
 0490            using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream);
 0491            JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions);
 0492        }
 493
 494        /// <summary>
 495        /// Called when [task completed].
 496        /// </summary>
 497        /// <param name="startTime">The start time.</param>
 498        /// <param name="endTime">The end time.</param>
 499        /// <param name="status">The status.</param>
 500        /// <param name="ex">The exception.</param>
 501        private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)
 502        {
 24503            var elapsedTime = endTime - startTime;
 504
 24505            _logger.LogInformation("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTim
 506
 24507            var result = new TaskResult
 24508            {
 24509                StartTimeUtc = startTime,
 24510                EndTimeUtc = endTime,
 24511                Status = status,
 24512                Name = Name,
 24513                Id = Id
 24514            };
 515
 24516            result.Key = ScheduledTask.Key;
 517
 24518            if (ex is not null)
 519            {
 0520                result.ErrorMessage = ex.Message;
 0521                result.LongErrorMessage = ex.StackTrace;
 522            }
 523
 24524            LastExecutionResult = result;
 525
 24526            ((TaskManager)_taskManager).OnTaskCompleted(this, result);
 24527        }
 528
 529        /// <inheritdoc />
 530        public void Dispose()
 531        {
 418532            Dispose(true);
 418533            GC.SuppressFinalize(this);
 418534        }
 535
 536        /// <summary>
 537        /// Releases unmanaged and - optionally - managed resources.
 538        /// </summary>
 539        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release o
 540        protected virtual void Dispose(bool dispose)
 541        {
 418542            if (dispose)
 543            {
 418544                DisposeTriggers();
 545
 418546                var wassRunning = State == TaskState.Running;
 418547                var startTime = CurrentExecutionStartTime;
 548
 418549                var token = CurrentCancellationTokenSource;
 418550                if (token is not null)
 551                {
 552                    try
 553                    {
 1554                        _logger.LogInformation("{Name}: Cancelling", Name);
 1555                        token.Cancel();
 1556                    }
 0557                    catch (Exception ex)
 558                    {
 0559                        _logger.LogError(ex, "Error calling CancellationToken.Cancel();");
 0560                    }
 561                }
 562
 418563                var task = _currentTask;
 418564                if (task is not null)
 565                {
 566                    try
 567                    {
 1568                        _logger.LogInformation("{Name}: Waiting on Task", Name);
 1569                        var exited = task.Wait(2000);
 570
 1571                        if (exited)
 572                        {
 1573                            _logger.LogInformation("{Name}: Task exited", Name);
 574                        }
 575                        else
 576                        {
 0577                            _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name);
 578                        }
 1579                    }
 0580                    catch (Exception ex)
 581                    {
 0582                        _logger.LogError(ex, "Error calling Task.WaitAll();");
 0583                    }
 584                }
 585
 418586                if (token is not null)
 587                {
 588                    try
 589                    {
 1590                        _logger.LogDebug("{Name}: Disposing CancellationToken", Name);
 1591                        token.Dispose();
 1592                    }
 0593                    catch (Exception ex)
 594                    {
 0595                        _logger.LogError(ex, "Error calling CancellationToken.Dispose();");
 0596                    }
 597                }
 598
 418599                if (wassRunning)
 600                {
 1601                    OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
 602                }
 603            }
 418604        }
 605
 606        /// <summary>
 607        /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger.
 608        /// </summary>
 609        /// <param name="info">The info.</param>
 610        /// <returns>BaseTaskTrigger.</returns>
 611        /// <exception cref="ArgumentException">Invalid trigger type:  + info.Type.</exception>
 612        private ITaskTrigger GetTrigger(TaskTriggerInfo info)
 613        {
 396614            var options = new TaskOptions
 396615            {
 396616                MaxRuntimeTicks = info.MaxRuntimeTicks
 396617            };
 618
 396619            if (info.Type.Equals(nameof(DailyTrigger), StringComparison.OrdinalIgnoreCase))
 620            {
 44621                if (!info.TimeOfDayTicks.HasValue)
 622                {
 0623                    throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
 624                }
 625
 44626                return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options);
 627            }
 628
 352629            if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))
 630            {
 0631                if (!info.TimeOfDayTicks.HasValue)
 632                {
 0633                    throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));
 634                }
 635
 0636                if (!info.DayOfWeek.HasValue)
 637                {
 0638                    throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info));
 639                }
 640
 0641                return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options);
 642            }
 643
 352644            if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))
 645            {
 286646                if (!info.IntervalTicks.HasValue)
 647                {
 0648                    throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info));
 649                }
 650
 286651                return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options);
 652            }
 653
 66654            if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))
 655            {
 66656                return new StartupTrigger(options);
 657            }
 658
 0659            throw new ArgumentException("Unrecognized trigger type: " + info.Type);
 660        }
 661
 662        /// <summary>
 663        /// Disposes each trigger.
 664        /// </summary>
 665        private void DisposeTriggers()
 666        {
 1628667            foreach (var triggerInfo in InternalTriggers)
 668            {
 396669                var trigger = triggerInfo.Item2;
 396670                trigger.Triggered -= OnTriggerTriggered;
 396671                trigger.Stop();
 396672                if (trigger is IDisposable disposable)
 673                {
 330674                    disposable.Dispose();
 675                }
 676            }
 418677        }
 678    }
 679}