aboutsummaryrefslogtreecommitdiff
path: root/Emby.Server.Implementations/ScheduledTasks
diff options
context:
space:
mode:
Diffstat (limited to 'Emby.Server.Implementations/ScheduledTasks')
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs14
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/DailyTrigger.cs91
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/IntervalTrigger.cs112
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs794
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/StartupTrigger.cs67
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/SystemEventTrigger.cs86
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/TaskManager.cs334
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs215
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs138
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs112
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/WeeklyTrigger.cs116
11 files changed, 2069 insertions, 10 deletions
diff --git a/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs
index 967e7ddd8..ec371c741 100644
--- a/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs
+++ b/Emby.Server.Implementations/ScheduledTasks/ChapterImagesTask.cs
@@ -15,6 +15,7 @@ using MediaBrowser.Controller.IO;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Tasks;
+using MediaBrowser.Model.Extensions;
namespace Emby.Server.Implementations.ScheduledTasks
{
@@ -123,16 +124,9 @@ namespace Emby.Server.Implementations.ScheduledTasks
try
{
- var chapters = _itemRepo.GetChapters(video.Id).ToList();
+ var chapters = _itemRepo.GetChapters(video.Id);
- var success = await _encodingManager.RefreshChapterImages(new ChapterImageRefreshOptions
- {
- SaveChapters = true,
- ExtractImages = extract,
- Video = video,
- Chapters = chapters
-
- }, CancellationToken.None);
+ var success = await _encodingManager.RefreshChapterImages(video, chapters, extract, true, CancellationToken.None);
if (!success)
{
@@ -142,7 +136,7 @@ namespace Emby.Server.Implementations.ScheduledTasks
_fileSystem.CreateDirectory(parentPath);
- _fileSystem.WriteAllText(failHistoryPath, string.Join("|", previouslyFailedImages.ToArray()));
+ _fileSystem.WriteAllText(failHistoryPath, string.Join("|", previouslyFailedImages.ToArray(previouslyFailedImages.Count)));
}
numComplete++;
diff --git a/Emby.Server.Implementations/ScheduledTasks/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/DailyTrigger.cs
new file mode 100644
index 000000000..1ba5d4329
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/DailyTrigger.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Globalization;
+using System.Threading;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Represents a task trigger that fires everyday
+ /// </summary>
+ public class DailyTrigger : ITaskTrigger
+ {
+ /// <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>
+ /// Gets the execution properties of this task.
+ /// </summary>
+ /// <value>
+ /// The execution properties of this task.
+ /// </value>
+ public TaskExecutionOptions TaskOptions { get; set; }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ /// <param name="lastResult">The last result.</param>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ DisposeTimer();
+
+ var now = DateTime.Now;
+
+ var triggerDate = now.TimeOfDay > TimeOfDay ? now.Date.AddDays(1) : now.Date;
+ triggerDate = triggerDate.Add(TimeOfDay);
+
+ var dueTime = triggerDate - now;
+
+ logger.Info("Daily trigger for {0} set to fire at {1}, which is {2} minutes from now.", taskName, triggerDate.ToString(), dueTime.TotalMinutes.ToString(CultureInfo.InvariantCulture));
+
+ Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ public void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ if (Timer != null)
+ {
+ Timer.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ if (Triggered != null)
+ {
+ Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/IntervalTrigger.cs
new file mode 100644
index 000000000..d09765e34
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/IntervalTrigger.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Linq;
+using System.Threading;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Represents a task trigger that runs repeatedly on an interval
+ /// </summary>
+ public class IntervalTrigger : ITaskTrigger
+ {
+ /// <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>
+ /// Gets the execution properties of this task.
+ /// </summary>
+ /// <value>
+ /// The execution properties of this task.
+ /// </value>
+ public TaskExecutionOptions TaskOptions { get; set; }
+
+ private DateTime _lastStartDate;
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ /// <param name="lastResult">The last result.</param>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ DisposeTimer();
+
+ DateTime triggerDate;
+
+ if (lastResult == null)
+ {
+ // Task has never been completed before
+ triggerDate = DateTime.UtcNow.AddHours(1);
+ }
+ else
+ {
+ triggerDate = new[] { lastResult.EndTimeUtc, _lastStartDate }.Max().Add(Interval);
+ }
+
+ if (DateTime.UtcNow > triggerDate)
+ {
+ triggerDate = DateTime.UtcNow.AddMinutes(1);
+ }
+
+ var dueTime = triggerDate - DateTime.UtcNow;
+ var maxDueTime = TimeSpan.FromDays(7);
+
+ if (dueTime > maxDueTime)
+ {
+ dueTime = maxDueTime;
+ }
+
+ Timer = new Timer(state => OnTriggered(), null, dueTime, TimeSpan.FromMilliseconds(-1));
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ public void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ if (Timer != null)
+ {
+ Timer.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ DisposeTimer();
+
+ if (Triggered != null)
+ {
+ _lastStartDate = DateTime.UtcNow;
+ Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
new file mode 100644
index 000000000..d7d048110
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs
@@ -0,0 +1,794 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Common.Extensions;
+using MediaBrowser.Common.Progress;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Extensions;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.System;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Class ScheduledTaskWorker
+ /// </summary>
+ public class ScheduledTaskWorker : IScheduledTaskWorker
+ {
+ public event EventHandler<GenericEventArgs<double>> TaskProgress;
+
+ /// <summary>
+ /// Gets or sets the scheduled task.
+ /// </summary>
+ /// <value>The scheduled task.</value>
+ public IScheduledTask ScheduledTask { get; private set; }
+
+ /// <summary>
+ /// Gets or sets the json serializer.
+ /// </summary>
+ /// <value>The json serializer.</value>
+ private IJsonSerializer JsonSerializer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ private IApplicationPaths ApplicationPaths { get; set; }
+
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ private ILogger Logger { get; set; }
+
+ /// <summary>
+ /// Gets the task manager.
+ /// </summary>
+ /// <value>The task manager.</value>
+ private ITaskManager TaskManager { get; set; }
+ private readonly IFileSystem _fileSystem;
+ private readonly ISystemEvents _systemEvents;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
+ /// </summary>
+ /// <param name="scheduledTask">The scheduled task.</param>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="taskManager">The task manager.</param>
+ /// <param name="jsonSerializer">The json serializer.</param>
+ /// <param name="logger">The logger.</param>
+ /// <exception cref="System.ArgumentNullException">
+ /// scheduledTask
+ /// or
+ /// applicationPaths
+ /// or
+ /// taskManager
+ /// or
+ /// jsonSerializer
+ /// or
+ /// logger
+ /// </exception>
+ public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem, ISystemEvents systemEvents)
+ {
+ if (scheduledTask == null)
+ {
+ throw new ArgumentNullException("scheduledTask");
+ }
+ if (applicationPaths == null)
+ {
+ throw new ArgumentNullException("applicationPaths");
+ }
+ if (taskManager == null)
+ {
+ throw new ArgumentNullException("taskManager");
+ }
+ if (jsonSerializer == null)
+ {
+ throw new ArgumentNullException("jsonSerializer");
+ }
+ if (logger == null)
+ {
+ throw new ArgumentNullException("logger");
+ }
+
+ ScheduledTask = scheduledTask;
+ ApplicationPaths = applicationPaths;
+ TaskManager = taskManager;
+ JsonSerializer = jsonSerializer;
+ Logger = logger;
+ _fileSystem = fileSystem;
+ _systemEvents = systemEvents;
+
+ InitTriggerEvents();
+ }
+
+ private bool _readFromFile = false;
+ /// <summary>
+ /// The _last execution result
+ /// </summary>
+ private TaskResult _lastExecutionResult;
+ /// <summary>
+ /// The _last execution result sync lock
+ /// </summary>
+ private readonly object _lastExecutionResultSyncLock = new object();
+ /// <summary>
+ /// Gets the last execution result.
+ /// </summary>
+ /// <value>The last execution result.</value>
+ public TaskResult LastExecutionResult
+ {
+ get
+ {
+ var path = GetHistoryFilePath();
+
+ lock (_lastExecutionResultSyncLock)
+ {
+ if (_lastExecutionResult == null && !_readFromFile)
+ {
+ try
+ {
+ _lastExecutionResult = JsonSerializer.DeserializeFromFile<TaskResult>(path);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // File doesn't exist. No biggie
+ }
+ catch (FileNotFoundException)
+ {
+ // File doesn't exist. No biggie
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error deserializing {0}", ex, path);
+ }
+ _readFromFile = true;
+ }
+ }
+
+ return _lastExecutionResult;
+ }
+ private set
+ {
+ _lastExecutionResult = value;
+
+ var path = GetHistoryFilePath();
+ _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
+
+ lock (_lastExecutionResultSyncLock)
+ {
+ JsonSerializer.SerializeToFile(value, path);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return ScheduledTask.Name; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description
+ {
+ get { return ScheduledTask.Description; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string Category
+ {
+ get { return ScheduledTask.Category; }
+ }
+
+ /// <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 double? CurrentProgress { get; private set; }
+
+ /// <summary>
+ /// The _triggers
+ /// </summary>
+ private Tuple<TaskTriggerInfo,ITaskTrigger>[] _triggers;
+ /// <summary>
+ /// Gets the triggers that define when the task will run
+ /// </summary>
+ /// <value>The triggers.</value>
+ private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers
+ {
+ get
+ {
+ return _triggers;
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ // Cleanup current triggers
+ if (_triggers != null)
+ {
+ DisposeTriggers();
+ }
+
+ _triggers = value.ToArray();
+
+ ReloadTriggerEvents(false);
+ }
+ }
+
+ /// <summary>
+ /// Gets the triggers that define when the task will run
+ /// </summary>
+ /// <value>The triggers.</value>
+ /// <exception cref="System.ArgumentNullException">value</exception>
+ public TaskTriggerInfo[] Triggers
+ {
+ get
+ {
+ var triggers = InternalTriggers;
+ return triggers.Select(i => i.Item1).ToArray(triggers.Length);
+ }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
+ var triggerList = value.Where(i => i != null).ToArray();
+
+ SaveTriggers(triggerList);
+
+ InternalTriggers = triggerList.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray(triggerList.Length);
+ }
+ }
+
+ /// <summary>
+ /// The _id
+ /// </summary>
+ private string _id;
+
+ /// <summary>
+ /// Gets the unique id.
+ /// </summary>
+ /// <value>The unique id.</value>
+ public string Id
+ {
+ get
+ {
+ if (_id == null)
+ {
+ _id = ScheduledTask.GetType().FullName.GetMD5().ToString("N");
+ }
+
+ return _id;
+ }
+ }
+
+ private void InitTriggerEvents()
+ {
+ _triggers = LoadTriggers();
+ ReloadTriggerEvents(true);
+ }
+
+ public void ReloadTriggerEvents()
+ {
+ ReloadTriggerEvents(false);
+ }
+
+ /// <summary>
+ /// Reloads the trigger events.
+ /// </summary>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ private void ReloadTriggerEvents(bool isApplicationStartup)
+ {
+ foreach (var triggerInfo in InternalTriggers)
+ {
+ var trigger = triggerInfo.Item2;
+
+ trigger.Stop();
+
+ trigger.Triggered -= trigger_Triggered;
+ trigger.Triggered += trigger_Triggered;
+ trigger.Start(LastExecutionResult, Logger, Name, isApplicationStartup);
+ }
+ }
+
+ /// <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>
+ async void trigger_Triggered(object sender, GenericEventArgs<TaskExecutionOptions> e)
+ {
+ var trigger = (ITaskTrigger)sender;
+
+ var configurableTask = ScheduledTask as IConfigurableScheduledTask;
+
+ if (configurableTask != null && !configurableTask.IsEnabled)
+ {
+ return;
+ }
+
+ Logger.Info("{0} fired for task: {1}", trigger.GetType().Name, Name);
+
+ trigger.Stop();
+
+ TaskManager.QueueScheduledTask(ScheduledTask, e.Argument);
+
+ await Task.Delay(1000).ConfigureAwait(false);
+
+ trigger.Start(LastExecutionResult, Logger, Name, false);
+ }
+
+ private Task _currentTask;
+
+ /// <summary>
+ /// Executes the task
+ /// </summary>
+ /// <param name="options">Task options.</param>
+ /// <returns>Task.</returns>
+ /// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception>
+ public async Task Execute(TaskExecutionOptions options)
+ {
+ var task = Task.Run(async () => await ExecuteInternal(options).ConfigureAwait(false));
+
+ _currentTask = task;
+
+ try
+ {
+ await task.ConfigureAwait(false);
+ }
+ finally
+ {
+ _currentTask = null;
+ GC.Collect();
+ }
+ }
+
+ private async Task ExecuteInternal(TaskExecutionOptions options)
+ {
+ // Cancel the current execution, if any
+ if (CurrentCancellationTokenSource != null)
+ {
+ throw new InvalidOperationException("Cannot execute a Task that is already running");
+ }
+
+ var progress = new SimpleProgress<double>();
+
+ CurrentCancellationTokenSource = new CancellationTokenSource();
+
+ Logger.Info("Executing {0}", Name);
+
+ ((TaskManager)TaskManager).OnTaskExecuting(this);
+
+ progress.ProgressChanged += progress_ProgressChanged;
+
+ TaskCompletionStatus status;
+ CurrentExecutionStartTime = DateTime.UtcNow;
+
+ Exception failureException = null;
+
+ try
+ {
+ if (options != null && options.MaxRuntimeMs.HasValue)
+ {
+ CurrentCancellationTokenSource.CancelAfter(options.MaxRuntimeMs.Value);
+ }
+
+ var localTask = ScheduledTask.Execute(CurrentCancellationTokenSource.Token, progress);
+
+ await localTask.ConfigureAwait(false);
+
+ status = TaskCompletionStatus.Completed;
+ }
+ catch (OperationCanceledException)
+ {
+ status = TaskCompletionStatus.Cancelled;
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error", ex);
+
+ failureException = ex;
+
+ status = TaskCompletionStatus.Failed;
+ }
+
+ var startTime = CurrentExecutionStartTime;
+ var endTime = DateTime.UtcNow;
+
+ progress.ProgressChanged -= progress_ProgressChanged;
+ CurrentCancellationTokenSource.Dispose();
+ CurrentCancellationTokenSource = null;
+ CurrentProgress = null;
+
+ OnTaskCompleted(startTime, endTime, status, failureException);
+ }
+
+ /// <summary>
+ /// Progress_s the progress changed.
+ /// </summary>
+ /// <param name="sender">The sender.</param>
+ /// <param name="e">The e.</param>
+ void progress_ProgressChanged(object sender, double e)
+ {
+ CurrentProgress = e;
+
+ EventHelper.FireEventIfNotNull(TaskProgress, this, new GenericEventArgs<double>
+ {
+ Argument = e
+
+ }, Logger);
+ }
+
+ /// <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>
+ /// Gets the scheduled tasks configuration directory.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetScheduledTasksConfigurationDirectory()
+ {
+ return Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
+ }
+
+ /// <summary>
+ /// Gets the scheduled tasks data directory.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetScheduledTasksDataDirectory()
+ {
+ return Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks");
+ }
+
+ /// <summary>
+ /// Gets the history file path.
+ /// </summary>
+ /// <value>The history file path.</value>
+ private string GetHistoryFilePath()
+ {
+ return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js");
+ }
+
+ /// <summary>
+ /// Gets the configuration file path.
+ /// </summary>
+ /// <returns>System.String.</returns>
+ private string GetConfigurationFilePath()
+ {
+ return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js");
+ }
+
+ /// <summary>
+ /// Loads the triggers.
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ private Tuple<TaskTriggerInfo, ITaskTrigger>[] LoadTriggers()
+ {
+ // This null check is not great, but is needed to handle bad user input, or user mucking with the config file incorrectly
+ var settings = LoadTriggerSettings().Where(i => i != null).ToArray();
+
+ return settings.Select(i => new Tuple<TaskTriggerInfo, ITaskTrigger>(i, GetTrigger(i))).ToArray();
+ }
+
+ private TaskTriggerInfo[] LoadTriggerSettings()
+ {
+ try
+ {
+ var list = JsonSerializer.DeserializeFromFile<IEnumerable<TaskTriggerInfo>>(GetConfigurationFilePath());
+
+ if (list != null)
+ {
+ return list.ToArray();
+ }
+ }
+ catch (FileNotFoundException)
+ {
+ // File doesn't exist. No biggie. Return defaults.
+ return ScheduledTask.GetDefaultTriggers().ToArray();
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // File doesn't exist. No biggie. Return defaults.
+ }
+ return ScheduledTask.GetDefaultTriggers().ToArray();
+ }
+
+ /// <summary>
+ /// Saves the triggers.
+ /// </summary>
+ /// <param name="triggers">The triggers.</param>
+ private void SaveTriggers(TaskTriggerInfo[] triggers)
+ {
+ var path = GetConfigurationFilePath();
+
+ _fileSystem.CreateDirectory(_fileSystem.GetDirectoryName(path));
+
+ JsonSerializer.SerializeToFile(triggers, path);
+ }
+
+ /// <summary>
+ /// Called when [task completed].
+ /// </summary>
+ /// <param name="startTime">The start time.</param>
+ /// <param name="endTime">The end time.</param>
+ /// <param name="status">The status.</param>
+ private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)
+ {
+ 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
+ };
+
+ result.Key = ScheduledTask.Key;
+
+ if (ex != null)
+ {
+ result.ErrorMessage = ex.Message;
+ result.LongErrorMessage = ex.StackTrace;
+ }
+
+ LastExecutionResult = result;
+
+ ((TaskManager)TaskManager).OnTaskCompleted(this, result);
+ }
+
+ /// <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();
+
+ var wassRunning = State == TaskState.Running;
+ var startTime = CurrentExecutionStartTime;
+
+ var token = CurrentCancellationTokenSource;
+ if (token != null)
+ {
+ try
+ {
+ Logger.Info(Name + ": Cancelling");
+ token.Cancel();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error calling CancellationToken.Cancel();", ex);
+ }
+ }
+ var task = _currentTask;
+ if (task != null)
+ {
+ try
+ {
+ Logger.Info(Name + ": Waiting on Task");
+ var exited = Task.WaitAll(new[] { task }, 2000);
+
+ if (exited)
+ {
+ Logger.Info(Name + ": Task exited");
+ }
+ else
+ {
+ Logger.Info(Name + ": Timed out waiting for task to stop");
+ }
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error calling Task.WaitAll();", ex);
+ }
+ }
+
+ if (token != null)
+ {
+ try
+ {
+ Logger.Debug(Name + ": Disposing CancellationToken");
+ token.Dispose();
+ }
+ catch (Exception ex)
+ {
+ Logger.ErrorException("Error calling CancellationToken.Dispose();", ex);
+ }
+ }
+ if (wassRunning)
+ {
+ OnTaskCompleted(startTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
+ }
+ }
+ }
+
+ /// <summary>
+ /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger
+ /// </summary>
+ /// <param name="info">The info.</param>
+ /// <returns>BaseTaskTrigger.</returns>
+ /// <exception cref="System.ArgumentNullException"></exception>
+ /// <exception cref="System.ArgumentException">Invalid trigger type: + info.Type</exception>
+ private ITaskTrigger GetTrigger(TaskTriggerInfo info)
+ {
+ var options = new TaskExecutionOptions
+ {
+ MaxRuntimeMs = info.MaxRuntimeMs
+ };
+
+ 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),
+ TaskOptions = options
+ };
+ }
+
+ 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,
+ TaskOptions = options
+ };
+ }
+
+ 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),
+ TaskOptions = options
+ };
+ }
+
+ if (info.Type.Equals(typeof(SystemEventTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ if (!info.SystemEvent.HasValue)
+ {
+ throw new ArgumentNullException();
+ }
+
+ return new SystemEventTrigger(_systemEvents)
+ {
+ SystemEvent = info.SystemEvent.Value,
+ TaskOptions = options
+ };
+ }
+
+ if (info.Type.Equals(typeof(StartupTrigger).Name, StringComparison.OrdinalIgnoreCase))
+ {
+ return new StartupTrigger();
+ }
+
+ throw new ArgumentException("Unrecognized trigger type: " + info.Type);
+ }
+
+ /// <summary>
+ /// Disposes each trigger
+ /// </summary>
+ private void DisposeTriggers()
+ {
+ foreach (var triggerInfo in InternalTriggers)
+ {
+ var trigger = triggerInfo.Item2;
+ trigger.Triggered -= trigger_Triggered;
+ trigger.Stop();
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/StartupTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/StartupTrigger.cs
new file mode 100644
index 000000000..d708c905d
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/StartupTrigger.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Class StartupTaskTrigger
+ /// </summary>
+ public class StartupTrigger : ITaskTrigger
+ {
+ public int DelayMs { get; set; }
+
+ /// <summary>
+ /// Gets the execution properties of this task.
+ /// </summary>
+ /// <value>
+ /// The execution properties of this task.
+ /// </value>
+ public TaskExecutionOptions TaskOptions { get; set; }
+
+ public StartupTrigger()
+ {
+ DelayMs = 3000;
+ }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ /// <param name="lastResult">The last result.</param>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ public async void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ if (isApplicationStartup)
+ {
+ await Task.Delay(DelayMs).ConfigureAwait(false);
+
+ OnTriggered();
+ }
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ public void Stop()
+ {
+ }
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ if (Triggered != null)
+ {
+ Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/SystemEventTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/SystemEventTrigger.cs
new file mode 100644
index 000000000..976754a40
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/SystemEventTrigger.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Threading.Tasks;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.System;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Class SystemEventTrigger
+ /// </summary>
+ public class SystemEventTrigger : ITaskTrigger
+ {
+ /// <summary>
+ /// Gets or sets the system event.
+ /// </summary>
+ /// <value>The system event.</value>
+ public SystemEvent SystemEvent { get; set; }
+
+ /// <summary>
+ /// Gets the execution properties of this task.
+ /// </summary>
+ /// <value>
+ /// The execution properties of this task.
+ /// </value>
+ public TaskExecutionOptions TaskOptions { get; set; }
+
+ private readonly ISystemEvents _systemEvents;
+
+ public SystemEventTrigger(ISystemEvents systemEvents)
+ {
+ _systemEvents = systemEvents;
+ }
+
+ /// <summary>
+ /// Stars waiting for the trigger action
+ /// </summary>
+ /// <param name="lastResult">The last result.</param>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ switch (SystemEvent)
+ {
+ case SystemEvent.WakeFromSleep:
+ _systemEvents.Resume += _systemEvents_Resume;
+ break;
+ }
+ }
+
+ private async void _systemEvents_Resume(object sender, EventArgs e)
+ {
+ if (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(10000).ConfigureAwait(false);
+
+ OnTriggered();
+ }
+ }
+
+ /// <summary>
+ /// Stops waiting for the trigger action
+ /// </summary>
+ public void Stop()
+ {
+ _systemEvents.Resume -= _systemEvents_Resume;
+ }
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ if (Triggered != null)
+ {
+ Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
new file mode 100644
index 000000000..5f9bf3731
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs
@@ -0,0 +1,334 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Events;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Serialization;
+using MediaBrowser.Model.System;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Class TaskManager
+ /// </summary>
+ public class TaskManager : ITaskManager
+ {
+ public event EventHandler<GenericEventArgs<IScheduledTaskWorker>> TaskExecuting;
+ public event EventHandler<TaskCompletionEventArgs> TaskCompleted;
+
+ /// <summary>
+ /// Gets the list of Scheduled Tasks
+ /// </summary>
+ /// <value>The scheduled tasks.</value>
+ public IScheduledTaskWorker[] ScheduledTasks { get; private set; }
+
+ /// <summary>
+ /// The _task queue
+ /// </summary>
+ private readonly ConcurrentQueue<Tuple<Type, TaskExecutionOptions>> _taskQueue =
+ new ConcurrentQueue<Tuple<Type, TaskExecutionOptions>>();
+
+ /// <summary>
+ /// Gets or sets the json serializer.
+ /// </summary>
+ /// <value>The json serializer.</value>
+ private IJsonSerializer JsonSerializer { get; set; }
+
+ /// <summary>
+ /// Gets or sets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ private IApplicationPaths ApplicationPaths { get; set; }
+
+ private readonly ISystemEvents _systemEvents;
+
+ /// <summary>
+ /// Gets the logger.
+ /// </summary>
+ /// <value>The logger.</value>
+ private ILogger Logger { get; set; }
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="TaskManager" /> class.
+ /// </summary>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="jsonSerializer">The json serializer.</param>
+ /// <param name="logger">The logger.</param>
+ /// <exception cref="System.ArgumentException">kernel</exception>
+ public TaskManager(IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem, ISystemEvents systemEvents)
+ {
+ ApplicationPaths = applicationPaths;
+ JsonSerializer = jsonSerializer;
+ Logger = logger;
+ _fileSystem = fileSystem;
+ _systemEvents = systemEvents;
+
+ ScheduledTasks = new IScheduledTaskWorker[] { };
+ }
+
+ private void BindToSystemEvent()
+ {
+ _systemEvents.Resume += _systemEvents_Resume;
+ }
+
+ private void _systemEvents_Resume(object sender, EventArgs e)
+ {
+ foreach (var task in ScheduledTasks)
+ {
+ task.ReloadTriggerEvents();
+ }
+ }
+
+ /// <summary>
+ /// Cancels if running and queue.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="options">Task options.</param>
+ public void CancelIfRunningAndQueue<T>(TaskExecutionOptions options)
+ where T : IScheduledTask
+ {
+ var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
+ ((ScheduledTaskWorker)task).CancelIfRunning();
+
+ QueueScheduledTask<T>(options);
+ }
+
+ public void CancelIfRunningAndQueue<T>()
+ where T : IScheduledTask
+ {
+ CancelIfRunningAndQueue<T>(new TaskExecutionOptions());
+ }
+
+ /// <summary>
+ /// Cancels if running
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ public void CancelIfRunning<T>()
+ where T : IScheduledTask
+ {
+ var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
+ ((ScheduledTaskWorker)task).CancelIfRunning();
+ }
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <typeparam name="T"></typeparam>
+ /// <param name="options">Task options</param>
+ public void QueueScheduledTask<T>(TaskExecutionOptions options)
+ where T : IScheduledTask
+ {
+ var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T));
+
+ if (scheduledTask == null)
+ {
+ Logger.Error("Unable to find scheduled task of type {0} in QueueScheduledTask.", typeof(T).Name);
+ }
+ else
+ {
+ QueueScheduledTask(scheduledTask, options);
+ }
+ }
+
+ public void QueueScheduledTask<T>()
+ where T : IScheduledTask
+ {
+ QueueScheduledTask<T>(new TaskExecutionOptions());
+ }
+
+ public void QueueIfNotRunning<T>()
+ where T : IScheduledTask
+ {
+ var task = ScheduledTasks.First(t => t.ScheduledTask.GetType() == typeof(T));
+
+ if (task.State != TaskState.Running)
+ {
+ QueueScheduledTask<T>(new TaskExecutionOptions());
+ }
+ }
+
+ public void Execute<T>()
+ where T : IScheduledTask
+ {
+ var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == typeof(T));
+
+ if (scheduledTask == null)
+ {
+ Logger.Error("Unable to find scheduled task of type {0} in Execute.", typeof(T).Name);
+ }
+ else
+ {
+ var type = scheduledTask.ScheduledTask.GetType();
+
+ Logger.Info("Queueing task {0}", type.Name);
+
+ lock (_taskQueue)
+ {
+ if (scheduledTask.State == TaskState.Idle)
+ {
+ Execute(scheduledTask, new TaskExecutionOptions());
+ }
+ }
+ }
+ }
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <param name="task">The task.</param>
+ /// <param name="options">The task options.</param>
+ public void QueueScheduledTask(IScheduledTask task, TaskExecutionOptions options)
+ {
+ var scheduledTask = ScheduledTasks.FirstOrDefault(t => t.ScheduledTask.GetType() == task.GetType());
+
+ if (scheduledTask == null)
+ {
+ Logger.Error("Unable to find scheduled task of type {0} in QueueScheduledTask.", task.GetType().Name);
+ }
+ else
+ {
+ QueueScheduledTask(scheduledTask, options);
+ }
+ }
+
+ /// <summary>
+ /// Queues the scheduled task.
+ /// </summary>
+ /// <param name="task">The task.</param>
+ /// <param name="options">The task options.</param>
+ private void QueueScheduledTask(IScheduledTaskWorker task, TaskExecutionOptions options)
+ {
+ var type = task.ScheduledTask.GetType();
+
+ Logger.Info("Queueing task {0}", type.Name);
+
+ lock (_taskQueue)
+ {
+ if (task.State == TaskState.Idle)
+ {
+ Execute(task, options);
+ return;
+ }
+
+ _taskQueue.Enqueue(new Tuple<Type, TaskExecutionOptions>(type, options));
+ }
+ }
+
+ /// <summary>
+ /// Adds the tasks.
+ /// </summary>
+ /// <param name="tasks">The tasks.</param>
+ public void AddTasks(IEnumerable<IScheduledTask> tasks)
+ {
+ var myTasks = ScheduledTasks.ToList();
+
+ var list = tasks.ToList();
+ myTasks.AddRange(list.Select(t => new ScheduledTaskWorker(t, ApplicationPaths, this, JsonSerializer, Logger, _fileSystem, _systemEvents)));
+
+ ScheduledTasks = myTasks.ToArray();
+
+ BindToSystemEvent();
+ }
+
+ /// <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)
+ {
+ foreach (var task in ScheduledTasks)
+ {
+ task.Dispose();
+ }
+ }
+
+ public void Cancel(IScheduledTaskWorker task)
+ {
+ ((ScheduledTaskWorker)task).Cancel();
+ }
+
+ public Task Execute(IScheduledTaskWorker task, TaskExecutionOptions options)
+ {
+ return ((ScheduledTaskWorker)task).Execute(options);
+ }
+
+ /// <summary>
+ /// Called when [task executing].
+ /// </summary>
+ /// <param name="task">The task.</param>
+ internal void OnTaskExecuting(IScheduledTaskWorker task)
+ {
+ EventHelper.FireEventIfNotNull(TaskExecuting, this, new GenericEventArgs<IScheduledTaskWorker>
+ {
+ Argument = task
+
+ }, Logger);
+ }
+
+ /// <summary>
+ /// Called when [task completed].
+ /// </summary>
+ /// <param name="task">The task.</param>
+ /// <param name="result">The result.</param>
+ internal void OnTaskCompleted(IScheduledTaskWorker task, TaskResult result)
+ {
+ EventHelper.FireEventIfNotNull(TaskCompleted, task, new TaskCompletionEventArgs
+ {
+ Result = result,
+ Task = task
+
+ }, Logger);
+
+ ExecuteQueuedTasks();
+ }
+
+ /// <summary>
+ /// Executes the queued tasks.
+ /// </summary>
+ private void ExecuteQueuedTasks()
+ {
+ Logger.Info("ExecuteQueuedTasks");
+
+ // Execute queued tasks
+ lock (_taskQueue)
+ {
+ var list = new List<Tuple<Type, TaskExecutionOptions>>();
+
+ Tuple<Type, TaskExecutionOptions> item;
+ while (_taskQueue.TryDequeue(out item))
+ {
+ if (list.All(i => i.Item1 != item.Item1))
+ {
+ list.Add(item);
+ }
+ }
+
+ foreach (var enqueuedType in list)
+ {
+ var scheduledTask = ScheduledTasks.First(t => t.ScheduledTask.GetType() == enqueuedType.Item1);
+
+ if (scheduledTask.State == TaskState.Idle)
+ {
+ Execute(scheduledTask, enqueuedType.Item2);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
new file mode 100644
index 000000000..701358fd4
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs
@@ -0,0 +1,215 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Deletes old cache files
+ /// </summary>
+ public class DeleteCacheFileTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ /// <summary>
+ /// Gets or sets the application paths.
+ /// </summary>
+ /// <value>The application paths.</value>
+ private IApplicationPaths ApplicationPaths { get; set; }
+
+ private readonly ILogger _logger;
+
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeleteCacheFileTask" /> class.
+ /// </summary>
+ public DeleteCacheFileTask(IApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem)
+ {
+ ApplicationPaths = appPaths;
+ _logger = logger;
+ _fileSystem = fileSystem;
+ }
+
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[] {
+
+ // Every so often
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ var minDateModified = DateTime.UtcNow.AddDays(-30);
+
+ try
+ {
+ DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.CachePath, minDateModified, progress);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // No biggie here. Nothing to delete
+ }
+
+ progress.Report(90);
+
+ minDateModified = DateTime.UtcNow.AddDays(-1);
+
+ try
+ {
+ DeleteCacheFilesFromDirectory(cancellationToken, ApplicationPaths.TempDirectory, minDateModified, progress);
+ }
+ catch (DirectoryNotFoundException)
+ {
+ // No biggie here. Nothing to delete
+ }
+
+ return Task.FromResult(true);
+ }
+
+
+ /// <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<double> progress)
+ {
+ var filesToDelete = _fileSystem.GetFiles(directory, true)
+ .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
+ .ToList();
+
+ var index = 0;
+
+ foreach (var file in filesToDelete)
+ {
+ double percent = index;
+ percent /= filesToDelete.Count;
+
+ progress.Report(100 * percent);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ DeleteFile(file.FullName);
+
+ index++;
+ }
+
+ DeleteEmptyFolders(directory);
+
+ progress.Report(100);
+ }
+
+ private void DeleteEmptyFolders(string parent)
+ {
+ foreach (var directory in _fileSystem.GetDirectoryPaths(parent))
+ {
+ DeleteEmptyFolders(directory);
+ if (!_fileSystem.GetFileSystemEntryPaths(directory).Any())
+ {
+ try
+ {
+ _fileSystem.DeleteDirectory(directory, false);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ _logger.ErrorException("Error deleting directory {0}", ex, directory);
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error deleting directory {0}", ex, directory);
+ }
+ }
+ }
+ }
+
+ private void DeleteFile(string path)
+ {
+ try
+ {
+ _fileSystem.DeleteFile(path);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ _logger.ErrorException("Error deleting file {0}", ex, path);
+ }
+ catch (IOException ex)
+ {
+ _logger.ErrorException("Error deleting file {0}", ex, path);
+ }
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return "Cache file cleanup"; }
+ }
+
+ public string Key
+ {
+ get { return "DeleteCacheFiles"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description
+ {
+ get { return "Deletes cache files no longer needed by the system"; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string Category
+ {
+ get
+ {
+ return "Maintenance";
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is hidden.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ public bool IsHidden
+ {
+ get { return true; }
+ }
+
+ public bool IsEnabled
+ {
+ get { return true; }
+ }
+
+ public bool IsLogged
+ {
+ get { return true; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
new file mode 100644
index 000000000..f98b09659
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Deletes old log files
+ /// </summary>
+ public class DeleteLogFileTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ /// <summary>
+ /// Gets or sets the configuration manager.
+ /// </summary>
+ /// <value>The configuration manager.</value>
+ private IConfigurationManager ConfigurationManager { get; set; }
+
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="DeleteLogFileTask" /> class.
+ /// </summary>
+ /// <param name="configurationManager">The configuration manager.</param>
+ public DeleteLogFileTask(IConfigurationManager configurationManager, IFileSystem fileSystem)
+ {
+ ConfigurationManager = configurationManager;
+ _fileSystem = fileSystem;
+ }
+
+ /// <summary>
+ /// Creates the triggers that define when the task will run
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[] {
+
+ // Every so often
+ new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks}
+ };
+ }
+
+ /// <summary>
+ /// Returns the task to be executed
+ /// </summary>
+ /// <param name="cancellationToken">The cancellation token.</param>
+ /// <param name="progress">The progress.</param>
+ /// <returns>Task.</returns>
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ // Delete log files more than n days old
+ var minDateModified = DateTime.UtcNow.AddDays(-ConfigurationManager.CommonConfiguration.LogFileRetentionDays);
+
+ var filesToDelete = _fileSystem.GetFiles(ConfigurationManager.CommonApplicationPaths.LogDirectoryPath, true)
+ .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified)
+ .ToList();
+
+ var index = 0;
+
+ foreach (var file in filesToDelete)
+ {
+ double percent = index;
+ percent /= filesToDelete.Count;
+
+ progress.Report(100 * percent);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ _fileSystem.DeleteFile(file.FullName);
+
+ index++;
+ }
+
+ progress.Report(100);
+
+ return Task.FromResult(true);
+ }
+
+ public string Key
+ {
+ get { return "CleanLogFiles"; }
+ }
+
+ /// <summary>
+ /// Gets the name of the task
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return "Log file cleanup"; }
+ }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description
+ {
+ get { return string.Format("Deletes log files that are more than {0} days old.", ConfigurationManager.CommonConfiguration.LogFileRetentionDays); }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string Category
+ {
+ get
+ {
+ return "Maintenance";
+ }
+ }
+
+ /// <summary>
+ /// Gets a value indicating whether this instance is hidden.
+ /// </summary>
+ /// <value><c>true</c> if this instance is hidden; otherwise, <c>false</c>.</value>
+ public bool IsHidden
+ {
+ get { return true; }
+ }
+
+ public bool IsEnabled
+ {
+ get { return true; }
+ }
+
+ public bool IsLogged
+ {
+ get { return true; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs
new file mode 100644
index 000000000..032fa05a0
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ReloadLoggerFileTask.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks
+{
+ /// <summary>
+ /// Class ReloadLoggerFileTask
+ /// </summary>
+ public class ReloadLoggerFileTask : IScheduledTask, IConfigurableScheduledTask
+ {
+ /// <summary>
+ /// Gets or sets the log manager.
+ /// </summary>
+ /// <value>The log manager.</value>
+ private ILogManager LogManager { get; set; }
+ /// <summary>
+ /// Gets or sets the configuration manager.
+ /// </summary>
+ /// <value>The configuration manager.</value>
+ private IConfigurationManager ConfigurationManager { get; set; }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="ReloadLoggerFileTask" /> class.
+ /// </summary>
+ /// <param name="logManager">The logManager.</param>
+ /// <param name="configurationManager">The configuration manager.</param>
+ public ReloadLoggerFileTask(ILogManager logManager, IConfigurationManager configurationManager)
+ {
+ LogManager = logManager;
+ ConfigurationManager = configurationManager;
+ }
+
+ /// <summary>
+ /// Gets the default triggers.
+ /// </summary>
+ /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ var trigger = new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerDaily, TimeOfDayTicks = TimeSpan.FromHours(0).Ticks }; //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>
+ public Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ progress.Report(0);
+
+ LogManager.ReloadLogger(ConfigurationManager.CommonConfiguration.EnableDebugLevelLogging
+ ? LogSeverity.Debug
+ : LogSeverity.Info);
+
+ return Task.FromResult(true);
+ }
+
+ /// <summary>
+ /// Gets the name.
+ /// </summary>
+ /// <value>The name.</value>
+ public string Name
+ {
+ get { return "Start new log file"; }
+ }
+
+ public string Key { get; }
+
+ /// <summary>
+ /// Gets the description.
+ /// </summary>
+ /// <value>The description.</value>
+ public string Description
+ {
+ get { return "Moves logging to a new file to help reduce log file sizes."; }
+ }
+
+ /// <summary>
+ /// Gets the category.
+ /// </summary>
+ /// <value>The category.</value>
+ public string Category
+ {
+ get { return "Application"; }
+ }
+
+ public bool IsHidden
+ {
+ get { return true; }
+ }
+
+ public bool IsEnabled
+ {
+ get { return true; }
+ }
+
+ public bool IsLogged
+ {
+ get { return true; }
+ }
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/WeeklyTrigger.cs
new file mode 100644
index 000000000..1a944ebf2
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/WeeklyTrigger.cs
@@ -0,0 +1,116 @@
+using System;
+using System.Threading;
+using MediaBrowser.Model.Events;
+using MediaBrowser.Model.Logging;
+using MediaBrowser.Model.Tasks;
+
+namespace Emby.Server.Implementations.ScheduledTasks
+{
+ /// <summary>
+ /// Represents a task trigger that fires on a weekly basis
+ /// </summary>
+ public class WeeklyTrigger : ITaskTrigger
+ {
+ /// <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 the execution properties of this task.
+ /// </summary>
+ /// <value>
+ /// The execution properties of this task.
+ /// </value>
+ public TaskExecutionOptions TaskOptions { 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>
+ /// <param name="lastResult">The last result.</param>
+ /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
+ public void Start(TaskResult lastResult, ILogger logger, string taskName, bool isApplicationStartup)
+ {
+ 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>
+ public void Stop()
+ {
+ DisposeTimer();
+ }
+
+ /// <summary>
+ /// Disposes the timer.
+ /// </summary>
+ private void DisposeTimer()
+ {
+ if (Timer != null)
+ {
+ Timer.Dispose();
+ }
+ }
+
+ /// <summary>
+ /// Occurs when [triggered].
+ /// </summary>
+ public event EventHandler<GenericEventArgs<TaskExecutionOptions>> Triggered;
+
+ /// <summary>
+ /// Called when [triggered].
+ /// </summary>
+ private void OnTriggered()
+ {
+ if (Triggered != null)
+ {
+ Triggered(this, new GenericEventArgs<TaskExecutionOptions>(TaskOptions));
+ }
+ }
+ }
+}