using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Events; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.ScheduledTasks; using MediaBrowser.Model.Events; using MediaBrowser.Model.Logging; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using CommonIO; namespace MediaBrowser.Common.Implementations.ScheduledTasks { /// /// Class ScheduledTaskWorker /// public class ScheduledTaskWorker : IScheduledTaskWorker { public event EventHandler> TaskProgress; /// /// Gets or sets the scheduled task. /// /// The scheduled task. public IScheduledTask ScheduledTask { get; private set; } /// /// Gets or sets the json serializer. /// /// The json serializer. private IJsonSerializer JsonSerializer { get; set; } /// /// Gets or sets the application paths. /// /// The application paths. private IApplicationPaths ApplicationPaths { get; set; } /// /// Gets the logger. /// /// The logger. private ILogger Logger { get; set; } /// /// Gets the task manager. /// /// The task manager. private ITaskManager TaskManager { get; set; } private readonly IFileSystem _fileSystem; /// /// Initializes a new instance of the class. /// /// The scheduled task. /// The application paths. /// The task manager. /// The json serializer. /// The logger. /// /// scheduledTask /// or /// applicationPaths /// or /// taskManager /// or /// jsonSerializer /// or /// logger /// public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger, IFileSystem fileSystem) { 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; InitTriggerEvents(); } private bool _readFromFile = false; /// /// The _last execution result /// private TaskResult _lastExecutionResult; /// /// The _last execution result sync lock /// private readonly object _lastExecutionResultSyncLock = new object(); /// /// Gets the last execution result. /// /// The last execution result. public TaskResult LastExecutionResult { get { var path = GetHistoryFilePath(); lock (_lastExecutionResultSyncLock) { if (_lastExecutionResult == null && !_readFromFile) { try { _lastExecutionResult = JsonSerializer.DeserializeFromFile(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(Path.GetDirectoryName(path)); lock (_lastExecutionResultSyncLock) { JsonSerializer.SerializeToFile(value, path); } } } /// /// Gets the name. /// /// The name. public string Name { get { return ScheduledTask.Name; } } /// /// Gets the description. /// /// The description. public string Description { get { return ScheduledTask.Description; } } /// /// Gets the category. /// /// The category. public string Category { get { return ScheduledTask.Category; } } /// /// Gets the current cancellation token /// /// The current cancellation token source. private CancellationTokenSource CurrentCancellationTokenSource { get; set; } /// /// Gets or sets the current execution start time. /// /// The current execution start time. private DateTime CurrentExecutionStartTime { get; set; } /// /// Gets the state. /// /// The state. public TaskState State { get { if (CurrentCancellationTokenSource != null) { return CurrentCancellationTokenSource.IsCancellationRequested ? TaskState.Cancelling : TaskState.Running; } return TaskState.Idle; } } /// /// Gets the current progress. /// /// The current progress. public double? CurrentProgress { get; private set; } /// /// The _triggers /// private Tuple[] _triggers; /// /// Gets the triggers that define when the task will run /// /// The triggers. private Tuple[] InternalTriggers { get { return _triggers; } set { if (value == null) { throw new ArgumentNullException("value"); } // Cleanup current triggers if (_triggers != null) { DisposeTriggers(); } _triggers = value.ToArray(); ReloadTriggerEvents(false); } } /// /// Gets the triggers that define when the task will run /// /// The triggers. /// value public TaskTriggerInfo[] Triggers { get { return InternalTriggers.Select(i => i.Item1).ToArray(); } set { if (value == null) { throw new ArgumentNullException("value"); } SaveTriggers(value); InternalTriggers = value.Select(i => new Tuple(i, GetTrigger(i))).ToArray(); } } /// /// The _id /// private string _id; /// /// Gets the unique id. /// /// The unique id. 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); } /// /// Reloads the trigger events. /// /// if set to true [is application startup]. 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); } } /// /// Handles the Triggered event of the trigger control. /// /// The source of the event. /// The instance containing the event data. async void trigger_Triggered(object sender, GenericEventArgs 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; /// /// Executes the task /// /// Task options. /// Task. /// Cannot execute a Task that is already running public async Task Execute(TaskExecutionOptions options) { var task = ExecuteInternal(options); _currentTask = task; try { await task.ConfigureAwait(false); } finally { _currentTask = null; } } 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 Progress(); 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); } /// /// Progress_s the progress changed. /// /// The sender. /// The e. void progress_ProgressChanged(object sender, double e) { CurrentProgress = e; EventHelper.FireEventIfNotNull(TaskProgress, this, new GenericEventArgs { Argument = e }, Logger); } /// /// Stops the task if it is currently executing /// /// Cannot cancel a Task unless it is in the Running state. public void Cancel() { if (State != TaskState.Running) { throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state."); } CancelIfRunning(); } /// /// Cancels if running. /// public void CancelIfRunning() { if (State == TaskState.Running) { Logger.Info("Attempting to cancel Scheduled Task {0}", Name); CurrentCancellationTokenSource.Cancel(); } } /// /// Gets the scheduled tasks configuration directory. /// /// System.String. private string GetScheduledTasksConfigurationDirectory() { return Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks"); } /// /// Gets the scheduled tasks data directory. /// /// System.String. private string GetScheduledTasksDataDirectory() { return Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks"); } /// /// Gets the history file path. /// /// The history file path. private string GetHistoryFilePath() { return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js"); } /// /// Gets the configuration file path. /// /// System.String. private string GetConfigurationFilePath() { return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js"); } /// /// Loads the triggers. /// /// IEnumerable{BaseTaskTrigger}. private Tuple[] LoadTriggers() { var settings = LoadTriggerSettings(); return settings.Select(i => new Tuple(i, GetTrigger(i))).ToArray(); } private TaskTriggerInfo[] LoadTriggerSettings() { try { return JsonSerializer.DeserializeFromFile>(GetConfigurationFilePath()) .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(); } } /// /// Saves the triggers. /// /// The triggers. private void SaveTriggers(TaskTriggerInfo[] triggers) { var path = GetConfigurationFilePath(); _fileSystem.CreateDirectory(Path.GetDirectoryName(path)); JsonSerializer.SerializeToFile(triggers, path); } /// /// Called when [task completed]. /// /// The start time. /// The end time. /// The status. 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); } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool dispose) { if (dispose) { DisposeTriggers(); 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); } } } /// /// Converts a TaskTriggerInfo into a concrete BaseTaskTrigger /// /// The info. /// BaseTaskTrigger. /// /// Invalid trigger type: + info.Type public static 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 { 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); } /// /// Disposes each trigger /// private void DisposeTriggers() { foreach (var triggerInfo in InternalTriggers) { var trigger = triggerInfo.Item2; trigger.Triggered -= trigger_Triggered; trigger.Stop(); } } } }