| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763 | #nullable disable#pragma warning disable CS1591using System;using System.Globalization;using System.IO;using System.Linq;using System.Text.Json;using System.Threading;using System.Threading.Tasks;using Emby.Server.Implementations.ScheduledTasks.Triggers;using Jellyfin.Data.Events;using Jellyfin.Extensions.Json;using MediaBrowser.Common.Configuration;using MediaBrowser.Common.Extensions;using MediaBrowser.Common.Progress;using MediaBrowser.Model.Tasks;using Microsoft.Extensions.Logging;namespace Emby.Server.Implementations.ScheduledTasks{    /// <summary>    /// Class ScheduledTaskWorker.    /// </summary>    public class ScheduledTaskWorker : IScheduledTaskWorker    {        /// <summary>        /// The options for the json Serializer.        /// </summary>        private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;        /// <summary>        /// Gets or sets the application paths.        /// </summary>        /// <value>The application paths.</value>        private readonly IApplicationPaths _applicationPaths;        /// <summary>        /// Gets or sets the logger.        /// </summary>        /// <value>The logger.</value>        private readonly ILogger _logger;        /// <summary>        /// Gets or sets the task manager.        /// </summary>        /// <value>The task manager.</value>        private readonly ITaskManager _taskManager;        /// <summary>        /// The _last execution result sync lock.        /// </summary>        private readonly object _lastExecutionResultSyncLock = new object();        private bool _readFromFile = false;        /// <summary>        /// The _last execution result.        /// </summary>        private TaskResult _lastExecutionResult;        private Task _currentTask;        /// <summary>        /// The _triggers.        /// </summary>        private Tuple<TaskTriggerInfo, ITaskTrigger>[] _triggers;        /// <summary>        /// The _id.        /// </summary>        private string _id;        /// <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="logger">The logger.</param>        /// <exception cref="ArgumentNullException">        /// scheduledTask        /// or        /// applicationPaths        /// or        /// taskManager        /// or        /// jsonSerializer        /// or        /// logger.        /// </exception>        public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, ILogger logger)        {            if (scheduledTask == null)            {                throw new ArgumentNullException(nameof(scheduledTask));            }            if (applicationPaths == null)            {                throw new ArgumentNullException(nameof(applicationPaths));            }            if (taskManager == null)            {                throw new ArgumentNullException(nameof(taskManager));            }            if (logger == null)            {                throw new ArgumentNullException(nameof(logger));            }            ScheduledTask = scheduledTask;            _applicationPaths = applicationPaths;            _taskManager = taskManager;            _logger = logger;            InitTriggerEvents();        }        public event EventHandler<GenericEventArgs<double>> TaskProgress;        /// <summary>        /// Gets the scheduled task.        /// </summary>        /// <value>The scheduled task.</value>        public IScheduledTask ScheduledTask { get; private set; }        /// <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)                    {                        if (File.Exists(path))                        {                            var bytes = File.ReadAllBytes(path);                            if (bytes.Length > 0)                            {                                try                                {                                    _lastExecutionResult = JsonSerializer.Deserialize<TaskResult>(bytes, _jsonOptions);                                }                                catch (JsonException ex)                                {                                    _logger.LogError(ex, "Error deserializing {File}", path);                                }                            }                            else                            {                                _logger.LogDebug("Scheduled Task history file {Path} is empty. Skipping deserialization.", path);                            }                        }                        _readFromFile = true;                    }                }                return _lastExecutionResult;            }            private set            {                _lastExecutionResult = value;                var path = GetHistoryFilePath();                Directory.CreateDirectory(Path.GetDirectoryName(path));                lock (_lastExecutionResultSyncLock)                {                    using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);                    using Utf8JsonWriter jsonStream = new Utf8JsonWriter(createStream);                    JsonSerializer.Serialize(jsonStream, value, _jsonOptions);                }            }        }        /// <summary>        /// Gets the name.        /// </summary>        /// <value>The name.</value>        public string Name => ScheduledTask.Name;        /// <summary>        /// Gets the description.        /// </summary>        /// <value>The description.</value>        public string Description => ScheduledTask.Description;        /// <summary>        /// Gets the category.        /// </summary>        /// <value>The category.</value>        public string Category => ScheduledTask.Category;        /// <summary>        /// Gets or sets 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>        /// Gets or sets the triggers that define when the task will run.        /// </summary>        /// <value>The triggers.</value>        private Tuple<TaskTriggerInfo, ITaskTrigger>[] InternalTriggers        {            get => _triggers;            set            {                if (value == null)                {                    throw new ArgumentNullException(nameof(value));                }                // Cleanup current triggers                if (_triggers != null)                {                    DisposeTriggers();                }                _triggers = value.ToArray();                ReloadTriggerEvents(false);            }        }        /// <summary>        /// Gets or sets the triggers that define when the task will run.        /// </summary>        /// <value>The triggers.</value>        /// <exception cref="ArgumentNullException"><c>value</c> is <c>null</c>.</exception>        public TaskTriggerInfo[] Triggers        {            get            {                var triggers = InternalTriggers;                return triggers.Select(i => i.Item1).ToArray();            }            set            {                if (value == null)                {                    throw new ArgumentNullException(nameof(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();            }        }        /// <summary>        /// Gets the unique id.        /// </summary>        /// <value>The unique id.</value>        public string Id        {            get            {                return _id ??= ScheduledTask.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture);            }        }        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 -= OnTriggerTriggered;                trigger.Triggered += OnTriggerTriggered;                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>        private async void OnTriggerTriggered(object sender, EventArgs e)        {            var trigger = (ITaskTrigger)sender;            if (ScheduledTask is IConfigurableScheduledTask configurableTask && !configurableTask.IsEnabled)            {                return;            }            _logger.LogInformation("{0} fired for task: {1}", trigger.GetType().Name, Name);            trigger.Stop();            _taskManager.QueueScheduledTask(ScheduledTask, trigger.TaskOptions);            await Task.Delay(1000).ConfigureAwait(false);            trigger.Start(LastExecutionResult, _logger, Name, false);        }        /// <summary>        /// Executes the task.        /// </summary>        /// <param name="options">Task options.</param>        /// <returns>Task.</returns>        /// <exception cref="InvalidOperationException">Cannot execute a Task that is already running.</exception>        public async Task Execute(TaskOptions 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(TaskOptions 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.LogInformation("Executing {0}", Name);            ((TaskManager)_taskManager).OnTaskExecuting(this);            progress.ProgressChanged += OnProgressChanged;            TaskCompletionStatus status;            CurrentExecutionStartTime = DateTime.UtcNow;            Exception failureException = null;            try            {                if (options != null && options.MaxRuntimeTicks.HasValue)                {                    CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value));                }                await ScheduledTask.Execute(CurrentCancellationTokenSource.Token, progress).ConfigureAwait(false);                status = TaskCompletionStatus.Completed;            }            catch (OperationCanceledException)            {                status = TaskCompletionStatus.Cancelled;            }            catch (Exception ex)            {                _logger.LogError(ex, "Error");                failureException = ex;                status = TaskCompletionStatus.Failed;            }            var startTime = CurrentExecutionStartTime;            var endTime = DateTime.UtcNow;            progress.ProgressChanged -= OnProgressChanged;            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>        private void OnProgressChanged(object sender, double e)        {            e = Math.Min(e, 100);            CurrentProgress = e;            TaskProgress?.Invoke(this, new GenericEventArgs<double>(e));        }        /// <summary>        /// Stops the task if it is currently executing.        /// </summary>        /// <exception cref="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.LogInformation("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()        {            string path = GetConfigurationFilePath();            TaskTriggerInfo[] list = null;            if (File.Exists(path))            {                var bytes = File.ReadAllBytes(path);                list = JsonSerializer.Deserialize<TaskTriggerInfo[]>(bytes, _jsonOptions);            }            // Return defaults if file doesn't exist.            return list ?? GetDefaultTriggers();        }        private TaskTriggerInfo[] GetDefaultTriggers()        {            try            {                return ScheduledTask.GetDefaultTriggers().ToArray();            }            catch            {                return new TaskTriggerInfo[]                {                    new TaskTriggerInfo                    {                        IntervalTicks = TimeSpan.FromDays(1).Ticks,                        Type = TaskTriggerInfo.TriggerInterval                    }                };            }        }        /// <summary>        /// Saves the triggers.        /// </summary>        /// <param name="triggers">The triggers.</param>        private void SaveTriggers(TaskTriggerInfo[] triggers)        {            var path = GetConfigurationFilePath();            Directory.CreateDirectory(Path.GetDirectoryName(path));            using FileStream createStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);            using Utf8JsonWriter jsonWriter = new Utf8JsonWriter(createStream);            JsonSerializer.Serialize(jsonWriter, triggers, _jsonOptions);        }        /// <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>        /// <param name="ex">The exception.</param>        private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)        {            var elapsedTime = endTime - startTime;            _logger.LogInformation("{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.LogInformation("{Name}: Cancelling", Name);                        token.Cancel();                    }                    catch (Exception ex)                    {                        _logger.LogError(ex, "Error calling CancellationToken.Cancel();");                    }                }                var task = _currentTask;                if (task != null)                {                    try                    {                        _logger.LogInformation("{Name}: Waiting on Task", Name);                        var exited = task.Wait(2000);                        if (exited)                        {                            _logger.LogInformation("{Name}: Task exited", Name);                        }                        else                        {                            _logger.LogInformation("{Name}: Timed out waiting for task to stop", Name);                        }                    }                    catch (Exception ex)                    {                        _logger.LogError(ex, "Error calling Task.WaitAll();");                    }                }                if (token != null)                {                    try                    {                        _logger.LogDebug("{Name}: Disposing CancellationToken", Name);                        token.Dispose();                    }                    catch (Exception ex)                    {                        _logger.LogError(ex, "Error calling CancellationToken.Dispose();");                    }                }                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="ArgumentException">Invalid trigger type:  + info.Type.</exception>        private ITaskTrigger GetTrigger(TaskTriggerInfo info)        {            var options = new TaskOptions            {                MaxRuntimeTicks = info.MaxRuntimeTicks            };            if (info.Type.Equals(nameof(DailyTrigger), StringComparison.OrdinalIgnoreCase))            {                if (!info.TimeOfDayTicks.HasValue)                {                    throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));                }                return new DailyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), options);            }            if (info.Type.Equals(nameof(WeeklyTrigger), StringComparison.OrdinalIgnoreCase))            {                if (!info.TimeOfDayTicks.HasValue)                {                    throw new ArgumentException("Info did not contain a TimeOfDayTicks.", nameof(info));                }                if (!info.DayOfWeek.HasValue)                {                    throw new ArgumentException("Info did not contain a DayOfWeek.", nameof(info));                }                return new WeeklyTrigger(TimeSpan.FromTicks(info.TimeOfDayTicks.Value), info.DayOfWeek.Value, options);            }            if (info.Type.Equals(nameof(IntervalTrigger), StringComparison.OrdinalIgnoreCase))            {                if (!info.IntervalTicks.HasValue)                {                    throw new ArgumentException("Info did not contain a IntervalTicks.", nameof(info));                }                return new IntervalTrigger(TimeSpan.FromTicks(info.IntervalTicks.Value), options);            }            if (info.Type.Equals(nameof(StartupTrigger), StringComparison.OrdinalIgnoreCase))            {                return new StartupTrigger(options);            }            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 -= OnTriggerTriggered;                trigger.Stop();            }        }    }}
 |