using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Kernel; using MediaBrowser.Common.Serialization; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Tasks; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace MediaBrowser.Common.ScheduledTasks { /// /// Represents a task that can be executed at a scheduled time /// /// The type of the T kernel type. public abstract class BaseScheduledTask : IScheduledTask where TKernelType : class, IKernel { /// /// Gets the kernel. /// /// The kernel. protected TKernelType Kernel { get; private set; } /// /// Gets the logger. /// /// The logger. protected ILogger Logger { get; private set; } /// /// Gets the task manager. /// /// The task manager. protected ITaskManager TaskManager { get; private set; } /// /// Initializes a new instance of the class. /// /// The kernel. /// The task manager. /// The logger. /// kernel protected BaseScheduledTask(TKernelType kernel, ITaskManager taskManager, ILogger logger) { if (kernel == null) { throw new ArgumentNullException("kernel"); } if (taskManager == null) { throw new ArgumentNullException("taskManager"); } if (logger == null) { throw new ArgumentNullException("logger"); } Kernel = kernel; TaskManager = taskManager; Logger = logger; ReloadTriggerEvents(true); } /// /// The _last execution result /// private TaskResult _lastExecutionResult; /// /// The _last execution resultinitialized /// private bool _lastExecutionResultinitialized; /// /// The _last execution result sync lock /// private object _lastExecutionResultSyncLock = new object(); /// /// Gets the last execution result. /// /// The last execution result. public TaskResult LastExecutionResult { get { LazyInitializer.EnsureInitialized(ref _lastExecutionResult, ref _lastExecutionResultinitialized, ref _lastExecutionResultSyncLock, () => { try { return JsonSerializer.DeserializeFromFile(HistoryFilePath); } catch (IOException) { // File doesn't exist. No biggie return null; } }); return _lastExecutionResult; } private set { _lastExecutionResult = value; _lastExecutionResultinitialized = value != null; } } /// /// The _scheduled tasks data directory /// private string _scheduledTasksDataDirectory; /// /// Gets the scheduled tasks data directory. /// /// The scheduled tasks data directory. private string ScheduledTasksDataDirectory { get { if (_scheduledTasksDataDirectory == null) { _scheduledTasksDataDirectory = Path.Combine(Kernel.ApplicationPaths.DataPath, "ScheduledTasks"); if (!Directory.Exists(_scheduledTasksDataDirectory)) { Directory.CreateDirectory(_scheduledTasksDataDirectory); } } return _scheduledTasksDataDirectory; } } /// /// The _scheduled tasks configuration directory /// private string _scheduledTasksConfigurationDirectory; /// /// Gets the scheduled tasks configuration directory. /// /// The scheduled tasks configuration directory. private string ScheduledTasksConfigurationDirectory { get { if (_scheduledTasksConfigurationDirectory == null) { _scheduledTasksConfigurationDirectory = Path.Combine(Kernel.ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); if (!Directory.Exists(_scheduledTasksConfigurationDirectory)) { Directory.CreateDirectory(_scheduledTasksConfigurationDirectory); } } return _scheduledTasksConfigurationDirectory; } } /// /// Gets the configuration file path. /// /// The configuration file path. private string ConfigurationFilePath { get { return Path.Combine(ScheduledTasksConfigurationDirectory, Id + ".js"); } } /// /// Gets the history file path. /// /// The history file path. private string HistoryFilePath { get { return Path.Combine(ScheduledTasksDataDirectory, Id + ".js"); } } /// /// Gets the current cancellation token /// /// The current cancellation token source. private CancellationTokenSource CurrentCancellationTokenSource { get; set; } /// /// Gets or sets the current execution start time. /// /// The current execution start time. private DateTime CurrentExecutionStartTime { get; set; } /// /// Gets the state. /// /// The state. public TaskState State { get { if (CurrentCancellationTokenSource != null) { return CurrentCancellationTokenSource.IsCancellationRequested ? TaskState.Cancelling : TaskState.Running; } return TaskState.Idle; } } /// /// Gets the current progress. /// /// The current progress. public double? CurrentProgress { get; private set; } /// /// The _triggers /// private IEnumerable _triggers; /// /// The _triggers initialized /// private bool _triggersInitialized; /// /// The _triggers sync lock /// private object _triggersSyncLock = new object(); /// /// Gets the triggers that define when the task will run /// /// The triggers. /// value public IEnumerable Triggers { get { LazyInitializer.EnsureInitialized(ref _triggers, ref _triggersInitialized, ref _triggersSyncLock, () => { try { return JsonSerializer.DeserializeFromFile>(ConfigurationFilePath) .Select(ScheduledTaskHelpers.GetTrigger) .ToList(); } catch (IOException) { // File doesn't exist. No biggie. Return defaults. return GetDefaultTriggers(); } }); return _triggers; } set { if (value == null) { throw new ArgumentNullException("value"); } // Cleanup current triggers if (_triggers != null) { DisposeTriggers(); } _triggers = value.ToList(); _triggersInitialized = true; ReloadTriggerEvents(false); JsonSerializer.SerializeToFile(_triggers.Select(ScheduledTaskHelpers.GetTriggerInfo), ConfigurationFilePath); } } /// /// Creates the triggers that define when the task will run /// /// IEnumerable{BaseTaskTrigger}. protected abstract IEnumerable GetDefaultTriggers(); /// /// Returns the task to be executed /// /// The cancellation token. /// The progress. /// Task. protected abstract Task ExecuteInternal(CancellationToken cancellationToken, IProgress progress); /// /// Gets the name of the task /// /// The name. public abstract string Name { get; } /// /// Gets the description. /// /// The description. public abstract string Description { get; } /// /// Gets the category. /// /// The category. public virtual string Category { get { return "Application"; } } /// /// The _id /// private Guid? _id; /// /// Gets the unique id. /// /// The unique id. public Guid Id { get { if (!_id.HasValue) { _id = GetType().FullName.GetMD5(); } return _id.Value; } } /// /// Reloads the trigger events. /// /// if set to true [is application startup]. private void ReloadTriggerEvents(bool isApplicationStartup) { foreach (var trigger in Triggers) { trigger.Stop(); trigger.Triggered -= trigger_Triggered; trigger.Triggered += trigger_Triggered; trigger.Start(isApplicationStartup); } } /// /// Handles the Triggered event of the trigger control. /// /// The source of the event. /// The instance containing the event data. void trigger_Triggered(object sender, EventArgs e) { var trigger = (BaseTaskTrigger)sender; Logger.Info("{0} fired for task: {1}", trigger.GetType().Name, Name); TaskManager.QueueScheduledTask(this); } /// /// Executes the task /// /// Task. /// Cannot execute a Task that is already running public async Task Execute() { // Cancel the current execution, if any if (CurrentCancellationTokenSource != null) { throw new InvalidOperationException("Cannot execute a Task that is already running"); } CurrentCancellationTokenSource = new CancellationTokenSource(); Logger.Info("Executing {0}", Name); var progress = new Progress(); progress.ProgressChanged += progress_ProgressChanged; TaskCompletionStatus status; CurrentExecutionStartTime = DateTime.UtcNow; Kernel.TcpManager.SendWebSocketMessage("ScheduledTaskBeginExecute", Name); try { await Task.Run(async () => await ExecuteInternal(CurrentCancellationTokenSource.Token, progress).ConfigureAwait(false)).ConfigureAwait(false); status = TaskCompletionStatus.Completed; } catch (OperationCanceledException) { status = TaskCompletionStatus.Cancelled; } catch (Exception ex) { Logger.ErrorException("Error", ex); status = TaskCompletionStatus.Failed; } var endTime = DateTime.UtcNow; LogResult(endTime, status); Kernel.TcpManager.SendWebSocketMessage("ScheduledTaskEndExecute", LastExecutionResult); progress.ProgressChanged -= progress_ProgressChanged; CurrentCancellationTokenSource.Dispose(); CurrentCancellationTokenSource = null; CurrentProgress = null; TaskManager.OnTaskCompleted(this); } /// /// Logs the result. /// /// The end time. /// The status. private void LogResult(DateTime endTime, TaskCompletionStatus status) { var startTime = CurrentExecutionStartTime; var elapsedTime = endTime - startTime; Logger.Info("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds); var result = new TaskResult { StartTimeUtc = startTime, EndTimeUtc = endTime, Status = status, Name = Name, Id = Id }; JsonSerializer.SerializeToFile(result, HistoryFilePath); LastExecutionResult = result; } /// /// Progress_s the progress changed. /// /// The sender. /// The e. void progress_ProgressChanged(object sender, double e) { CurrentProgress = e; } /// /// Stops the task if it is currently executing /// /// Cannot cancel a Task unless it is in the Running state. public void Cancel() { if (State != TaskState.Running) { throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state."); } CancelIfRunning(); } /// /// Cancels if running. /// public void CancelIfRunning() { if (State == TaskState.Running) { Logger.Info("Attempting to cancel Scheduled Task {0}", Name); CurrentCancellationTokenSource.Cancel(); } } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { if (dispose) { DisposeTriggers(); if (State == TaskState.Running) { LogResult(DateTime.UtcNow, TaskCompletionStatus.Aborted); } if (CurrentCancellationTokenSource != null) { CurrentCancellationTokenSource.Dispose(); } } } /// /// Disposes each trigger /// private void DisposeTriggers() { foreach (var trigger in Triggers) { trigger.Triggered -= trigger_Triggered; trigger.Dispose(); } } } }