diff options
Diffstat (limited to 'MediaBrowser.Common/ScheduledTasks')
14 files changed, 1632 insertions, 0 deletions
diff --git a/MediaBrowser.Common/ScheduledTasks/BaseScheduledTask.cs b/MediaBrowser.Common/ScheduledTasks/BaseScheduledTask.cs new file mode 100644 index 0000000000..61ed649e2c --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/BaseScheduledTask.cs @@ -0,0 +1,503 @@ +using MediaBrowser.Common.Extensions; +using MediaBrowser.Common.Kernel; +using MediaBrowser.Common.Logging; +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 +{ + /// <summary> + /// Represents a task that can be executed at a scheduled time + /// </summary> + /// <typeparam name="TKernelType">The type of the T kernel type.</typeparam> + public abstract class BaseScheduledTask<TKernelType> : IScheduledTask + where TKernelType : IKernel + { + /// <summary> + /// Gets the kernel. + /// </summary> + /// <value>The kernel.</value> + protected TKernelType Kernel { get; private set; } + + /// <summary> + /// The _last execution result + /// </summary> + private TaskResult _lastExecutionResult; + /// <summary> + /// The _last execution resultinitialized + /// </summary> + private bool _lastExecutionResultinitialized; + /// <summary> + /// The _last execution result sync lock + /// </summary> + private object _lastExecutionResultSyncLock = new object(); + /// <summary> + /// Gets the last execution result. + /// </summary> + /// <value>The last execution result.</value> + public TaskResult LastExecutionResult + { + get + { + LazyInitializer.EnsureInitialized(ref _lastExecutionResult, ref _lastExecutionResultinitialized, ref _lastExecutionResultSyncLock, () => + { + try + { + return JsonSerializer.DeserializeFromFile<TaskResult>(HistoryFilePath); + } + catch (IOException) + { + // File doesn't exist. No biggie + return null; + } + }); + + return _lastExecutionResult; + } + private set + { + _lastExecutionResult = value; + + _lastExecutionResultinitialized = value != null; + } + } + + /// <summary> + /// The _scheduled tasks data directory + /// </summary> + private string _scheduledTasksDataDirectory; + /// <summary> + /// Gets the scheduled tasks data directory. + /// </summary> + /// <value>The scheduled tasks data directory.</value> + private string ScheduledTasksDataDirectory + { + get + { + if (_scheduledTasksDataDirectory == null) + { + _scheduledTasksDataDirectory = Path.Combine(Kernel.ApplicationPaths.DataPath, "ScheduledTasks"); + + if (!Directory.Exists(_scheduledTasksDataDirectory)) + { + Directory.CreateDirectory(_scheduledTasksDataDirectory); + } + } + return _scheduledTasksDataDirectory; + } + } + + /// <summary> + /// The _scheduled tasks configuration directory + /// </summary> + private string _scheduledTasksConfigurationDirectory; + /// <summary> + /// Gets the scheduled tasks configuration directory. + /// </summary> + /// <value>The scheduled tasks configuration directory.</value> + private string ScheduledTasksConfigurationDirectory + { + get + { + if (_scheduledTasksConfigurationDirectory == null) + { + _scheduledTasksConfigurationDirectory = Path.Combine(Kernel.ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); + + if (!Directory.Exists(_scheduledTasksConfigurationDirectory)) + { + Directory.CreateDirectory(_scheduledTasksConfigurationDirectory); + } + } + return _scheduledTasksConfigurationDirectory; + } + } + + /// <summary> + /// Gets the configuration file path. + /// </summary> + /// <value>The configuration file path.</value> + private string ConfigurationFilePath + { + get { return Path.Combine(ScheduledTasksConfigurationDirectory, Id + ".js"); } + } + + /// <summary> + /// Gets the history file path. + /// </summary> + /// <value>The history file path.</value> + private string HistoryFilePath + { + get { return Path.Combine(ScheduledTasksDataDirectory, Id + ".js"); } + } + + /// <summary> + /// Gets the current cancellation token + /// </summary> + /// <value>The current cancellation token source.</value> + private CancellationTokenSource CurrentCancellationTokenSource { get; set; } + + /// <summary> + /// Gets or sets the current execution start time. + /// </summary> + /// <value>The current execution start time.</value> + private DateTime CurrentExecutionStartTime { get; set; } + + /// <summary> + /// Gets the state. + /// </summary> + /// <value>The state.</value> + public TaskState State + { + get + { + if (CurrentCancellationTokenSource != null) + { + return CurrentCancellationTokenSource.IsCancellationRequested + ? TaskState.Cancelling + : TaskState.Running; + } + + return TaskState.Idle; + } + } + + /// <summary> + /// Gets the current progress. + /// </summary> + /// <value>The current progress.</value> + public TaskProgress CurrentProgress { get; private set; } + + /// <summary> + /// The _triggers + /// </summary> + private IEnumerable<BaseTaskTrigger> _triggers; + /// <summary> + /// The _triggers initialized + /// </summary> + private bool _triggersInitialized; + /// <summary> + /// The _triggers sync lock + /// </summary> + private object _triggersSyncLock = new object(); + /// <summary> + /// Gets the triggers that define when the task will run + /// </summary> + /// <value>The triggers.</value> + /// <exception cref="System.ArgumentNullException">value</exception> + public IEnumerable<BaseTaskTrigger> Triggers + { + get + { + LazyInitializer.EnsureInitialized(ref _triggers, ref _triggersInitialized, ref _triggersSyncLock, () => + { + try + { + return JsonSerializer.DeserializeFromFile<IEnumerable<TaskTriggerInfo>>(ConfigurationFilePath) + .Select(t => ScheduledTaskHelpers.GetTrigger(t, Kernel)) + .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(); + + JsonSerializer.SerializeToFile(_triggers.Select(ScheduledTaskHelpers.GetTriggerInfo), ConfigurationFilePath); + } + } + + /// <summary> + /// Creates the triggers that define when the task will run + /// </summary> + /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + protected abstract IEnumerable<BaseTaskTrigger> GetDefaultTriggers(); + + /// <summary> + /// Returns the task to be executed + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task.</returns> + protected abstract Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress); + + /// <summary> + /// Gets the name of the task + /// </summary> + /// <value>The name.</value> + public abstract string Name { get; } + + /// <summary> + /// Gets the description. + /// </summary> + /// <value>The description.</value> + public abstract string Description { get; } + + /// <summary> + /// Gets the category. + /// </summary> + /// <value>The category.</value> + public virtual string Category + { + get { return "Application"; } + } + + /// <summary> + /// The _id + /// </summary> + private Guid? _id; + /// <summary> + /// Gets the unique id. + /// </summary> + /// <value>The unique id.</value> + public Guid Id + { + get + { + if (!_id.HasValue) + { + _id = GetType().FullName.GetMD5(); + } + + return _id.Value; + } + } + + /// <summary> + /// Gets the logger. + /// </summary> + /// <value>The logger.</value> + protected ILogger Logger { get; private set; } + + /// <summary> + /// Initializes the specified kernel. + /// </summary> + /// <param name="kernel">The kernel.</param> + public void Initialize(IKernel kernel) + { + Logger = LogManager.GetLogger(GetType().Name); + + Kernel = (TKernelType)kernel; + ReloadTriggerEvents(); + } + + /// <summary> + /// Reloads the trigger events. + /// </summary> + private void ReloadTriggerEvents() + { + foreach (var trigger in Triggers) + { + trigger.Stop(); + + trigger.Triggered -= trigger_Triggered; + trigger.Triggered += trigger_Triggered; + trigger.Start(); + } + } + + /// <summary> + /// Handles the Triggered event of the trigger control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param> + void trigger_Triggered(object sender, EventArgs e) + { + var trigger = (BaseTaskTrigger)sender; + + Logger.Info("{0} fired for task: {1}", trigger.GetType().Name, Name); + + Kernel.TaskManager.QueueScheduledTask(this); + } + + /// <summary> + /// Executes the task + /// </summary> + /// <returns>Task.</returns> + /// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception> + 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<TaskProgress>(); + + 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; + + Kernel.TaskManager.OnTaskCompleted(this); + } + + /// <summary> + /// Logs the result. + /// </summary> + /// <param name="endTime">The end time.</param> + /// <param name="status">The status.</param> + 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; + } + + /// <summary> + /// Progress_s the progress changed. + /// </summary> + /// <param name="sender">The sender.</param> + /// <param name="e">The e.</param> + void progress_ProgressChanged(object sender, TaskProgress e) + { + CurrentProgress = e; + } + + /// <summary> + /// Stops the task if it is currently executing + /// </summary> + /// <exception cref="System.InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception> + public void Cancel() + { + if (State != TaskState.Running) + { + throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state."); + } + + CancelIfRunning(); + } + + /// <summary> + /// Cancels if running. + /// </summary> + public void CancelIfRunning() + { + if (State == TaskState.Running) + { + Logger.Info("Attempting to cancel Scheduled Task {0}", Name); + CurrentCancellationTokenSource.Cancel(); + } + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + DisposeTriggers(); + + if (State == TaskState.Running) + { + LogResult(DateTime.UtcNow, TaskCompletionStatus.Aborted); + } + + if (CurrentCancellationTokenSource != null) + { + CurrentCancellationTokenSource.Dispose(); + } + } + } + + /// <summary> + /// Disposes each trigger + /// </summary> + private void DisposeTriggers() + { + foreach (var trigger in Triggers) + { + trigger.Triggered -= trigger_Triggered; + trigger.Dispose(); + } + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/BaseTaskTrigger.cs b/MediaBrowser.Common/ScheduledTasks/BaseTaskTrigger.cs new file mode 100644 index 0000000000..5e60bb718c --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/BaseTaskTrigger.cs @@ -0,0 +1,64 @@ +using System; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.ScheduledTasks +{ + /// <summary> + /// Use to indicate that a scheduled task should run + /// </summary> + public abstract class BaseTaskTrigger : IDisposable + { + /// <summary> + /// Fires when the trigger condition is satisfied and the task should run + /// </summary> + internal event EventHandler<EventArgs> Triggered; + + /// <summary> + /// Called when [triggered]. + /// </summary> + protected async void OnTriggered() + { + Stop(); + + if (Triggered != null) + { + Triggered(this, EventArgs.Empty); + } + + await Task.Delay(1000).ConfigureAwait(false); + + Start(); + } + + /// <summary> + /// Stars waiting for the trigger action + /// </summary> + protected internal abstract void Start(); + + /// <summary> + /// Stops waiting for the trigger action + /// </summary> + protected internal abstract void Stop(); + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + Stop(); + } + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/DailyTrigger.cs b/MediaBrowser.Common/ScheduledTasks/DailyTrigger.cs new file mode 100644 index 0000000000..c1cf1a9a33 --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/DailyTrigger.cs @@ -0,0 +1,71 @@ +using System; +using System.Threading; + +namespace MediaBrowser.Common.ScheduledTasks +{ + /// <summary> + /// Represents a task trigger that fires everyday + /// </summary> + public class DailyTrigger : BaseTaskTrigger + { + /// <summary> + /// Get the time of day to trigger the task to run + /// </summary> + /// <value>The time of day.</value> + public TimeSpan TimeOfDay { get; set; } + + /// <summary> + /// Gets or sets the timer. + /// </summary> + /// <value>The timer.</value> + private Timer Timer { get; set; } + + /// <summary> + /// Stars waiting for the trigger action + /// </summary> + protected internal override void Start() + { + DisposeTimer(); + + var now = DateTime.Now; + + var triggerDate = now.TimeOfDay > TimeOfDay ? now.Date.AddDays(1) : now.Date; + triggerDate = triggerDate.Add(TimeOfDay); + + Timer = new Timer(state => OnTriggered(), null, triggerDate - now, TimeSpan.FromMilliseconds(-1)); + } + + /// <summary> + /// Stops waiting for the trigger action + /// </summary> + protected internal override void Stop() + { + DisposeTimer(); + } + + /// <summary> + /// Disposes this instance. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected override void Dispose(bool dispose) + { + if (dispose) + { + DisposeTimer(); + } + + base.Dispose(dispose); + } + + /// <summary> + /// Disposes the timer. + /// </summary> + private void DisposeTimer() + { + if (Timer != null) + { + Timer.Dispose(); + } + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs b/MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs new file mode 100644 index 0000000000..9220116b2f --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/IScheduledTask.cs @@ -0,0 +1,86 @@ +using MediaBrowser.Common.Kernel; +using MediaBrowser.Model.Tasks; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.ScheduledTasks +{ + /// <summary> + /// Interface IScheduledTask + /// </summary> + public interface IScheduledTask : IDisposable + { + /// <summary> + /// Gets the triggers. + /// </summary> + /// <value>The triggers.</value> + IEnumerable<BaseTaskTrigger> Triggers { get; set; } + + /// <summary> + /// Gets the last execution result. + /// </summary> + /// <value>The last execution result.</value> + TaskResult LastExecutionResult { get; } + + /// <summary> + /// Gets the state. + /// </summary> + /// <value>The state.</value> + TaskState State { get; } + + /// <summary> + /// Gets the current progress. + /// </summary> + /// <value>The current progress.</value> + TaskProgress CurrentProgress { get; } + + /// <summary> + /// Gets the name of the task + /// </summary> + /// <value>The name.</value> + string Name { get; } + + /// <summary> + /// Gets the description. + /// </summary> + /// <value>The description.</value> + string Description { get; } + + /// <summary> + /// Gets the category. + /// </summary> + /// <value>The category.</value> + string Category { get; } + + /// <summary> + /// Gets the unique id. + /// </summary> + /// <value>The unique id.</value> + Guid Id { get; } + + /// <summary> + /// Executes the task + /// </summary> + /// <returns>Task.</returns> + /// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception> + Task Execute(); + + /// <summary> + /// Stops the task if it is currently executing + /// </summary> + /// <exception cref="System.InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception> + void Cancel(); + + /// <summary> + /// Initializes the specified kernel. + /// </summary> + /// <param name="kernel">The kernel.</param> + void Initialize(IKernel kernel); + + /// <summary> + /// Cancels if running. + /// </summary> + void CancelIfRunning(); + } +}
\ No newline at end of file diff --git a/MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs b/MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs new file mode 100644 index 0000000000..1ead484c84 --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/IntervalTrigger.cs @@ -0,0 +1,66 @@ +using System; +using System.Threading; + +namespace MediaBrowser.Common.ScheduledTasks +{ + /// <summary> + /// Represents a task trigger that runs repeatedly on an interval + /// </summary> + public class IntervalTrigger : BaseTaskTrigger + { + /// <summary> + /// Gets or sets the interval. + /// </summary> + /// <value>The interval.</value> + public TimeSpan Interval { get; set; } + + /// <summary> + /// Gets or sets the timer. + /// </summary> + /// <value>The timer.</value> + private Timer Timer { get; set; } + + /// <summary> + /// Stars waiting for the trigger action + /// </summary> + protected internal override void Start() + { + DisposeTimer(); + + Timer = new Timer(state => OnTriggered(), null, Interval, TimeSpan.FromMilliseconds(-1)); + } + + /// <summary> + /// Stops waiting for the trigger action + /// </summary> + protected internal override void Stop() + { + DisposeTimer(); + } + + /// <summary> + /// Disposes this instance. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected override void Dispose(bool dispose) + { + if (dispose) + { + DisposeTimer(); + } + + base.Dispose(dispose); + } + + /// <summary> + /// Disposes the timer. + /// </summary> + private void DisposeTimer() + { + if (Timer != null) + { + Timer.Dispose(); + } + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/ScheduledTaskHelpers.cs b/MediaBrowser.Common/ScheduledTasks/ScheduledTaskHelpers.cs new file mode 100644 index 0000000000..34421ca1fa --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/ScheduledTaskHelpers.cs @@ -0,0 +1,153 @@ +using MediaBrowser.Common.Kernel; +using MediaBrowser.Model.Tasks; +using System; +using System.Linq; + +namespace MediaBrowser.Common.ScheduledTasks +{ + /// <summary> + /// Class ScheduledTaskHelpers + /// </summary> + public static class ScheduledTaskHelpers + { + /// <summary> + /// Gets the task info. + /// </summary> + /// <param name="task">The task.</param> + /// <returns>TaskInfo.</returns> + public static TaskInfo GetTaskInfo(IScheduledTask task) + { + return new TaskInfo + { + Name = task.Name, + CurrentProgress = task.CurrentProgress, + State = task.State, + Id = task.Id, + LastExecutionResult = task.LastExecutionResult, + Triggers = task.Triggers.Select(GetTriggerInfo).ToArray(), + Description = task.Description, + Category = task.Category + }; + } + + /// <summary> + /// Gets the trigger info. + /// </summary> + /// <param name="trigger">The trigger.</param> + /// <returns>TaskTriggerInfo.</returns> + public static TaskTriggerInfo GetTriggerInfo(BaseTaskTrigger trigger) + { + var info = new TaskTriggerInfo + { + Type = trigger.GetType().Name + }; + + var dailyTrigger = trigger as DailyTrigger; + + if (dailyTrigger != null) + { + info.TimeOfDayTicks = dailyTrigger.TimeOfDay.Ticks; + } + + var weeklyTaskTrigger = trigger as WeeklyTrigger; + + if (weeklyTaskTrigger != null) + { + info.TimeOfDayTicks = weeklyTaskTrigger.TimeOfDay.Ticks; + info.DayOfWeek = weeklyTaskTrigger.DayOfWeek; + } + + var intervalTaskTrigger = trigger as IntervalTrigger; + + if (intervalTaskTrigger != null) + { + info.IntervalTicks = intervalTaskTrigger.Interval.Ticks; + } + + var systemEventTrigger = trigger as SystemEventTrigger; + + if (systemEventTrigger != null) + { + info.SystemEvent = systemEventTrigger.SystemEvent; + } + + return info; + } + + /// <summary> + /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger + /// </summary> + /// <param name="info">The info.</param> + /// <param name="kernel">The kernel.</param> + /// <returns>BaseTaskTrigger.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + /// <exception cref="System.ArgumentException">Invalid trigger type: + info.Type</exception> + public static BaseTaskTrigger GetTrigger(TaskTriggerInfo info, IKernel kernel) + { + if (info.Type.Equals(typeof(DailyTrigger).Name, StringComparison.OrdinalIgnoreCase)) + { + if (!info.TimeOfDayTicks.HasValue) + { + throw new ArgumentNullException(); + } + + return new DailyTrigger + { + TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value) + }; + } + + if (info.Type.Equals(typeof(WeeklyTrigger).Name, StringComparison.OrdinalIgnoreCase)) + { + if (!info.TimeOfDayTicks.HasValue) + { + throw new ArgumentNullException(); + } + + if (!info.DayOfWeek.HasValue) + { + throw new ArgumentNullException(); + } + + return new WeeklyTrigger + { + TimeOfDay = TimeSpan.FromTicks(info.TimeOfDayTicks.Value), + DayOfWeek = info.DayOfWeek.Value + }; + } + + if (info.Type.Equals(typeof(IntervalTrigger).Name, StringComparison.OrdinalIgnoreCase)) + { + if (!info.IntervalTicks.HasValue) + { + throw new ArgumentNullException(); + } + + return new IntervalTrigger + { + Interval = TimeSpan.FromTicks(info.IntervalTicks.Value) + }; + } + + if (info.Type.Equals(typeof(SystemEventTrigger).Name, StringComparison.OrdinalIgnoreCase)) + { + if (!info.SystemEvent.HasValue) + { + throw new ArgumentNullException(); + } + + return new SystemEventTrigger + { + SystemEvent = info.SystemEvent.Value + }; + } + + if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase)) + { + return new StartupTrigger(kernel); + } + + throw new ArgumentException("Unrecognized trigger type: " + info.Type); + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/StartupTrigger.cs b/MediaBrowser.Common/ScheduledTasks/StartupTrigger.cs new file mode 100644 index 0000000000..84775924ff --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/StartupTrigger.cs @@ -0,0 +1,50 @@ +using MediaBrowser.Common.Kernel; +using System; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.ScheduledTasks +{ + /// <summary> + /// Class StartupTaskTrigger + /// </summary> + public class StartupTrigger : BaseTaskTrigger + { + /// <summary> + /// Gets the kernel. + /// </summary> + /// <value>The kernel.</value> + protected IKernel Kernel { get; private set; } + + /// <summary> + /// Initializes a new instance of the <see cref="StartupTrigger" /> class. + /// </summary> + /// <param name="kernel">The kernel.</param> + public StartupTrigger(IKernel kernel) + { + Kernel = kernel; + } + + /// <summary> + /// Stars waiting for the trigger action + /// </summary> + protected internal override void Start() + { + Kernel.ReloadCompleted += Kernel_ReloadCompleted; + } + + async void Kernel_ReloadCompleted(object sender, EventArgs e) + { + await Task.Delay(2000).ConfigureAwait(false); + + OnTriggered(); + } + + /// <summary> + /// Stops waiting for the trigger action + /// </summary> + protected internal override void Stop() + { + Kernel.ReloadCompleted -= Kernel_ReloadCompleted; + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/SystemEventTrigger.cs b/MediaBrowser.Common/ScheduledTasks/SystemEventTrigger.cs new file mode 100644 index 0000000000..3075a8587b --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/SystemEventTrigger.cs @@ -0,0 +1,55 @@ +using MediaBrowser.Model.Tasks; +using Microsoft.Win32; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.ScheduledTasks +{ + /// <summary> + /// Class SystemEventTrigger + /// </summary> + public class SystemEventTrigger : BaseTaskTrigger + { + /// <summary> + /// Gets or sets the system event. + /// </summary> + /// <value>The system event.</value> + public SystemEvent SystemEvent { get; set; } + + /// <summary> + /// Stars waiting for the trigger action + /// </summary> + protected internal override void Start() + { + switch (SystemEvent) + { + case SystemEvent.WakeFromSleep: + SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; + break; + } + } + + /// <summary> + /// Stops waiting for the trigger action + /// </summary> + protected internal override void Stop() + { + SystemEvents.PowerModeChanged -= SystemEvents_PowerModeChanged; + } + + /// <summary> + /// Handles the PowerModeChanged event of the SystemEvents control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="PowerModeChangedEventArgs" /> instance containing the event data.</param> + async void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e) + { + if (e.Mode == PowerModes.Resume && SystemEvent == SystemEvent.WakeFromSleep) + { + // This value is a bit arbitrary, but add a delay to help ensure network connections have been restored before running the task + await Task.Delay(5000).ConfigureAwait(false); + + OnTriggered(); + } + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/TaskManager.cs b/MediaBrowser.Common/ScheduledTasks/TaskManager.cs new file mode 100644 index 0000000000..4e6407e7c6 --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/TaskManager.cs @@ -0,0 +1,108 @@ +using MediaBrowser.Common.Kernel; +using MediaBrowser.Model.Tasks; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MediaBrowser.Common.ScheduledTasks +{ + /// <summary> + /// Class TaskManager + /// </summary> + public class TaskManager : BaseManager<IKernel> + { + /// <summary> + /// The _task queue + /// </summary> + private readonly List<Type> _taskQueue = new List<Type>(); + + /// <summary> + /// Initializes a new instance of the <see cref="TaskManager" /> class. + /// </summary> + /// <param name="kernel">The kernel.</param> + public TaskManager(IKernel kernel) + : base(kernel) + { + + } + + /// <summary> + /// Cancels if running and queue. + /// </summary> + /// <typeparam name="T"></typeparam> + public void CancelIfRunningAndQueue<T>() + where T : IScheduledTask + { + Kernel.ScheduledTasks.OfType<T>().First().CancelIfRunning(); + QueueScheduledTask<T>(); + } + + /// <summary> + /// Queues the scheduled task. + /// </summary> + /// <typeparam name="T"></typeparam> + public void QueueScheduledTask<T>() + where T : IScheduledTask + { + var scheduledTask = Kernel.ScheduledTasks.OfType<T>().First(); + + QueueScheduledTask(scheduledTask); + } + + /// <summary> + /// Queues the scheduled task. + /// </summary> + /// <param name="task">The task.</param> + public void QueueScheduledTask(IScheduledTask task) + { + var type = task.GetType(); + + var scheduledTask = Kernel.ScheduledTasks.First(t => t.GetType() == type); + + lock (_taskQueue) + { + // If it's idle just execute immediately + if (scheduledTask.State == TaskState.Idle) + { + scheduledTask.Execute(); + return; + } + + if (!_taskQueue.Contains(type)) + { + Logger.Info("Queueing task {0}", type.Name); + _taskQueue.Add(type); + } + else + { + Logger.Info("Task already queued: {0}", type.Name); + } + } + } + + /// <summary> + /// Called when [task completed]. + /// </summary> + /// <param name="task">The task.</param> + internal void OnTaskCompleted(IScheduledTask task) + { + // Execute queued tasks + lock (_taskQueue) + { + var copy = _taskQueue.ToList(); + + foreach (var type in copy) + { + var scheduledTask = Kernel.ScheduledTasks.First(t => t.GetType() == type); + + if (scheduledTask.State == TaskState.Idle) + { + scheduledTask.Execute(); + + _taskQueue.Remove(type); + } + } + } + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteCacheFileTask.cs new file mode 100644 index 0000000000..98c6d672a5 --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -0,0 +1,109 @@ +using MediaBrowser.Common.Kernel; +using MediaBrowser.Model.Tasks; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.ScheduledTasks.Tasks +{ + /// <summary> + /// Deletes old cache files + /// </summary> + [Export(typeof(IScheduledTask))] + public class DeleteCacheFileTask : BaseScheduledTask<IKernel> + { + /// <summary> + /// Creates the triggers that define when the task will run + /// </summary> + /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers() + { + var trigger = new DailyTrigger { TimeOfDay = TimeSpan.FromHours(2) }; //2am + + return new[] { trigger }; + } + + /// <summary> + /// Returns the task to be executed + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task.</returns> + protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress) + { + return Task.Run(() => + { + var minDateModified = DateTime.UtcNow.AddMonths(-2); + + DeleteCacheFilesFromDirectory(cancellationToken, Kernel.ApplicationPaths.CachePath, minDateModified, progress); + }); + } + + + /// <summary> + /// Deletes the cache files from directory with a last write time less than a given date + /// </summary> + /// <param name="cancellationToken">The task cancellation token.</param> + /// <param name="directory">The directory.</param> + /// <param name="minDateModified">The min date modified.</param> + /// <param name="progress">The progress.</param> + private void DeleteCacheFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<TaskProgress> progress) + { + var filesToDelete = new DirectoryInfo(directory).EnumerateFileSystemInfos("*", SearchOption.AllDirectories) + .Where(f => !f.Attributes.HasFlag(FileAttributes.Directory) && f.LastWriteTimeUtc < minDateModified) + .ToList(); + + var index = 0; + + foreach (var file in filesToDelete) + { + double percent = index; + percent /= filesToDelete.Count; + + progress.Report(new TaskProgress { Description = file.FullName, PercentComplete = 100 * percent }); + + cancellationToken.ThrowIfCancellationRequested(); + + File.Delete(file.FullName); + + index++; + } + + progress.Report(new TaskProgress { PercentComplete = 100 }); + } + + /// <summary> + /// Gets the name of the task + /// </summary> + /// <value>The name.</value> + public override string Name + { + get { return "Cache file cleanup"; } + } + + /// <summary> + /// Gets the description. + /// </summary> + /// <value>The description.</value> + public override string Description + { + get { return "Deletes cache files no longer needed by the system"; } + } + + /// <summary> + /// Gets the category. + /// </summary> + /// <value>The category.</value> + public override string Category + { + get + { + return "Maintenance"; + } + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteLogFileTask.cs new file mode 100644 index 0000000000..afb21187ce --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -0,0 +1,97 @@ +using MediaBrowser.Common.Kernel; +using MediaBrowser.Model.Tasks; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.ScheduledTasks.Tasks +{ + /// <summary> + /// Deletes old log files + /// </summary> + [Export(typeof(IScheduledTask))] + public class DeleteLogFileTask : BaseScheduledTask<IKernel> + { + /// <summary> + /// Creates the triggers that define when the task will run + /// </summary> + /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers() + { + var trigger = new DailyTrigger { TimeOfDay = TimeSpan.FromHours(2) }; //2am + + return new[] { trigger }; + } + + /// <summary> + /// Returns the task to be executed + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task.</returns> + protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress) + { + return Task.Run(() => + { + // Delete log files more than n days old + var minDateModified = DateTime.UtcNow.AddDays(-(Kernel.Configuration.LogFileRetentionDays)); + + var filesToDelete = new DirectoryInfo(Kernel.ApplicationPaths.LogDirectoryPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories) + .Where(f => f.LastWriteTimeUtc < minDateModified) + .ToList(); + + var index = 0; + + foreach (var file in filesToDelete) + { + double percent = index; + percent /= filesToDelete.Count; + + progress.Report(new TaskProgress { Description = file.FullName, PercentComplete = 100 * percent }); + + cancellationToken.ThrowIfCancellationRequested(); + + File.Delete(file.FullName); + + index++; + } + + progress.Report(new TaskProgress { PercentComplete = 100 }); + }); + } + + /// <summary> + /// Gets the name of the task + /// </summary> + /// <value>The name.</value> + public override string Name + { + get { return "Log file cleanup"; } + } + + /// <summary> + /// Gets the description. + /// </summary> + /// <value>The description.</value> + public override string Description + { + get { return string.Format("Deletes log files that are more than {0} days old.", Kernel.Configuration.LogFileRetentionDays); } + } + + /// <summary> + /// Gets the category. + /// </summary> + /// <value>The category.</value> + public override string Category + { + get + { + return "Maintenance"; + } + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/Tasks/ReloadLoggerTask.cs b/MediaBrowser.Common/ScheduledTasks/Tasks/ReloadLoggerTask.cs new file mode 100644 index 0000000000..24aaad57f1 --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/Tasks/ReloadLoggerTask.cs @@ -0,0 +1,61 @@ +using MediaBrowser.Common.Kernel; +using MediaBrowser.Model.Tasks; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.ScheduledTasks.Tasks +{ + /// <summary> + /// Class ReloadLoggerFileTask + /// </summary> + [Export(typeof(IScheduledTask))] + public class ReloadLoggerFileTask : BaseScheduledTask<IKernel> + { + /// <summary> + /// Gets the default triggers. + /// </summary> + /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers() + { + var trigger = new DailyTrigger { TimeOfDay = TimeSpan.FromHours(0) }; //12am + + return new[] { trigger }; + } + + /// <summary> + /// Executes the internal. + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task.</returns> + protected override Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress) + { + cancellationToken.ThrowIfCancellationRequested(); + + progress.Report(new TaskProgress { PercentComplete = 0 }); + + return Task.Run(() => Kernel.ReloadLogger()); + } + + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public override string Name + { + get { return "Start new log file"; } + } + + /// <summary> + /// Gets the description. + /// </summary> + /// <value>The description.</value> + public override string Description + { + get { return "Moves logging to a new file to help reduce log file sizes."; } + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/Tasks/SystemUpdateTask.cs b/MediaBrowser.Common/ScheduledTasks/Tasks/SystemUpdateTask.cs new file mode 100644 index 0000000000..1412be2a24 --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/Tasks/SystemUpdateTask.cs @@ -0,0 +1,108 @@ +using MediaBrowser.Common.Kernel; +using MediaBrowser.Common.Logging; +using MediaBrowser.Common.Updates; +using MediaBrowser.Model.Tasks; +using System; +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Deployment.Application; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Common.ScheduledTasks.Tasks +{ + /// <summary> + /// Plugin Update Task + /// </summary> + [Export(typeof(IScheduledTask))] + public class SystemUpdateTask : BaseScheduledTask<IKernel> + { + /// <summary> + /// Creates the triggers that define when the task will run + /// </summary> + /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + protected override IEnumerable<BaseTaskTrigger> GetDefaultTriggers() + { + return new BaseTaskTrigger[] { + + // 1am + new DailyTrigger { TimeOfDay = TimeSpan.FromHours(1) }, + + new IntervalTrigger { Interval = TimeSpan.FromHours(2)} + }; + } + + /// <summary> + /// Returns the task to be executed + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task.</returns> + protected override async Task ExecuteInternal(CancellationToken cancellationToken, IProgress<TaskProgress> progress) + { + if (!ApplicationDeployment.IsNetworkDeployed) return; + + EventHandler<TaskProgress> innerProgressHandler = (sender, e) => progress.Report(new TaskProgress { PercentComplete = e.PercentComplete * .1 }); + + // Create a progress object for the update check + var innerProgress = new Progress<TaskProgress>(); + innerProgress.ProgressChanged += innerProgressHandler; + + var updateInfo = await new ApplicationUpdateCheck().CheckForApplicationUpdate(cancellationToken, innerProgress).ConfigureAwait(false); + + // Release the event handler + innerProgress.ProgressChanged -= innerProgressHandler; + + progress.Report(new TaskProgress { PercentComplete = 10 }); + + if (!updateInfo.UpdateAvailable) + { + progress.Report(new TaskProgress { PercentComplete = 100 }); + return; + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (Kernel.Configuration.EnableAutoUpdate) + { + Logger.Info("Update Revision {0} available. Updating...", updateInfo.AvailableVersion); + + innerProgressHandler = (sender, e) => progress.Report(new TaskProgress { PercentComplete = (e.PercentComplete * .9) + .1 }); + + innerProgress = new Progress<TaskProgress>(); + innerProgress.ProgressChanged += innerProgressHandler; + + await new ApplicationUpdater().UpdateApplication(cancellationToken, innerProgress).ConfigureAwait(false); + + // Release the event handler + innerProgress.ProgressChanged -= innerProgressHandler; + + Kernel.OnApplicationUpdated(updateInfo.AvailableVersion); + } + else + { + Logger.Info("A new version of Media Browser is available."); + } + + progress.Report(new TaskProgress { PercentComplete = 100 }); + } + + /// <summary> + /// Gets the name of the task + /// </summary> + /// <value>The name.</value> + public override string Name + { + get { return "Check for application updates"; } + } + + /// <summary> + /// Gets the description. + /// </summary> + /// <value>The description.</value> + public override string Description + { + get { return "Downloads and installs application updates."; } + } + } +} diff --git a/MediaBrowser.Common/ScheduledTasks/WeeklyTrigger.cs b/MediaBrowser.Common/ScheduledTasks/WeeklyTrigger.cs new file mode 100644 index 0000000000..136dc8b130 --- /dev/null +++ b/MediaBrowser.Common/ScheduledTasks/WeeklyTrigger.cs @@ -0,0 +1,101 @@ +using System; +using System.Threading; + +namespace MediaBrowser.Common.ScheduledTasks +{ + /// <summary> + /// Represents a task trigger that fires on a weekly basis + /// </summary> + public class WeeklyTrigger : BaseTaskTrigger + { + /// <summary> + /// Get the time of day to trigger the task to run + /// </summary> + /// <value>The time of day.</value> + public TimeSpan TimeOfDay { get; set; } + + /// <summary> + /// Gets or sets the day of week. + /// </summary> + /// <value>The day of week.</value> + public DayOfWeek DayOfWeek { get; set; } + + /// <summary> + /// Gets or sets the timer. + /// </summary> + /// <value>The timer.</value> + private Timer Timer { get; set; } + + /// <summary> + /// Stars waiting for the trigger action + /// </summary> + protected internal override void Start() + { + DisposeTimer(); + + var triggerDate = GetNextTriggerDateTime(); + + Timer = new Timer(state => OnTriggered(), null, triggerDate - DateTime.Now, TimeSpan.FromMilliseconds(-1)); + } + + /// <summary> + /// Gets the next trigger date time. + /// </summary> + /// <returns>DateTime.</returns> + private DateTime GetNextTriggerDateTime() + { + var now = DateTime.Now; + + // If it's on the same day + if (now.DayOfWeek == DayOfWeek) + { + // It's either later today, or a week from now + return now.TimeOfDay < TimeOfDay ? now.Date.Add(TimeOfDay) : now.Date.AddDays(7).Add(TimeOfDay); + } + + var triggerDate = now.Date; + + // Walk the date forward until we get to the trigger day + while (triggerDate.DayOfWeek != DayOfWeek) + { + triggerDate = triggerDate.AddDays(1); + } + + // Return the trigger date plus the time offset + return triggerDate.Add(TimeOfDay); + } + + /// <summary> + /// Stops waiting for the trigger action + /// </summary> + protected internal override void Stop() + { + DisposeTimer(); + } + + /// <summary> + /// Disposes this instance. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected override void Dispose(bool dispose) + { + if (dispose) + { + DisposeTimer(); + } + + base.Dispose(dispose); + } + + /// <summary> + /// Disposes the timer. + /// </summary> + private void DisposeTimer() + { + if (Timer != null) + { + Timer.Dispose(); + } + } + } +} |
