ScheduledTaskWorker.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. using MediaBrowser.Common.Configuration;
  2. using MediaBrowser.Common.Events;
  3. using MediaBrowser.Common.Extensions;
  4. using MediaBrowser.Common.ScheduledTasks;
  5. using MediaBrowser.Model.Events;
  6. using MediaBrowser.Model.Logging;
  7. using MediaBrowser.Model.Serialization;
  8. using MediaBrowser.Model.Tasks;
  9. using System;
  10. using System.Collections.Generic;
  11. using System.IO;
  12. using System.Linq;
  13. using System.Threading;
  14. using System.Threading.Tasks;
  15. namespace MediaBrowser.Common.Implementations.ScheduledTasks
  16. {
  17. /// <summary>
  18. /// Class ScheduledTaskWorker
  19. /// </summary>
  20. public class ScheduledTaskWorker : IScheduledTaskWorker
  21. {
  22. public event EventHandler<GenericEventArgs<double>> TaskProgress;
  23. /// <summary>
  24. /// Gets or sets the scheduled task.
  25. /// </summary>
  26. /// <value>The scheduled task.</value>
  27. public IScheduledTask ScheduledTask { get; private set; }
  28. /// <summary>
  29. /// Gets or sets the json serializer.
  30. /// </summary>
  31. /// <value>The json serializer.</value>
  32. private IJsonSerializer JsonSerializer { get; set; }
  33. /// <summary>
  34. /// Gets or sets the application paths.
  35. /// </summary>
  36. /// <value>The application paths.</value>
  37. private IApplicationPaths ApplicationPaths { get; set; }
  38. /// <summary>
  39. /// Gets the logger.
  40. /// </summary>
  41. /// <value>The logger.</value>
  42. private ILogger Logger { get; set; }
  43. /// <summary>
  44. /// Gets the task manager.
  45. /// </summary>
  46. /// <value>The task manager.</value>
  47. private ITaskManager TaskManager { get; set; }
  48. /// <summary>
  49. /// Initializes a new instance of the <see cref="ScheduledTaskWorker" /> class.
  50. /// </summary>
  51. /// <param name="scheduledTask">The scheduled task.</param>
  52. /// <param name="applicationPaths">The application paths.</param>
  53. /// <param name="taskManager">The task manager.</param>
  54. /// <param name="jsonSerializer">The json serializer.</param>
  55. /// <param name="logger">The logger.</param>
  56. /// <exception cref="System.ArgumentNullException">
  57. /// scheduledTask
  58. /// or
  59. /// applicationPaths
  60. /// or
  61. /// taskManager
  62. /// or
  63. /// jsonSerializer
  64. /// or
  65. /// logger
  66. /// </exception>
  67. public ScheduledTaskWorker(IScheduledTask scheduledTask, IApplicationPaths applicationPaths, ITaskManager taskManager, IJsonSerializer jsonSerializer, ILogger logger)
  68. {
  69. if (scheduledTask == null)
  70. {
  71. throw new ArgumentNullException("scheduledTask");
  72. }
  73. if (applicationPaths == null)
  74. {
  75. throw new ArgumentNullException("applicationPaths");
  76. }
  77. if (taskManager == null)
  78. {
  79. throw new ArgumentNullException("taskManager");
  80. }
  81. if (jsonSerializer == null)
  82. {
  83. throw new ArgumentNullException("jsonSerializer");
  84. }
  85. if (logger == null)
  86. {
  87. throw new ArgumentNullException("logger");
  88. }
  89. ScheduledTask = scheduledTask;
  90. ApplicationPaths = applicationPaths;
  91. TaskManager = taskManager;
  92. JsonSerializer = jsonSerializer;
  93. Logger = logger;
  94. ReloadTriggerEvents(true);
  95. }
  96. /// <summary>
  97. /// The _last execution result
  98. /// </summary>
  99. private TaskResult _lastExecutionResult;
  100. /// <summary>
  101. /// The _last execution resultinitialized
  102. /// </summary>
  103. private bool _lastExecutionResultinitialized;
  104. /// <summary>
  105. /// The _last execution result sync lock
  106. /// </summary>
  107. private object _lastExecutionResultSyncLock = new object();
  108. /// <summary>
  109. /// Gets the last execution result.
  110. /// </summary>
  111. /// <value>The last execution result.</value>
  112. public TaskResult LastExecutionResult
  113. {
  114. get
  115. {
  116. LazyInitializer.EnsureInitialized(ref _lastExecutionResult, ref _lastExecutionResultinitialized, ref _lastExecutionResultSyncLock, () =>
  117. {
  118. var path = GetHistoryFilePath();
  119. try
  120. {
  121. return JsonSerializer.DeserializeFromFile<TaskResult>(path);
  122. }
  123. catch (DirectoryNotFoundException)
  124. {
  125. // File doesn't exist. No biggie
  126. return null;
  127. }
  128. catch (FileNotFoundException)
  129. {
  130. // File doesn't exist. No biggie
  131. return null;
  132. }
  133. catch (Exception ex)
  134. {
  135. Logger.ErrorException("Error deserializing {0}", ex, path);
  136. return null;
  137. }
  138. });
  139. return _lastExecutionResult;
  140. }
  141. private set
  142. {
  143. _lastExecutionResult = value;
  144. _lastExecutionResultinitialized = value != null;
  145. }
  146. }
  147. /// <summary>
  148. /// Gets the name.
  149. /// </summary>
  150. /// <value>The name.</value>
  151. public string Name
  152. {
  153. get { return ScheduledTask.Name; }
  154. }
  155. /// <summary>
  156. /// Gets the description.
  157. /// </summary>
  158. /// <value>The description.</value>
  159. public string Description
  160. {
  161. get { return ScheduledTask.Description; }
  162. }
  163. /// <summary>
  164. /// Gets the category.
  165. /// </summary>
  166. /// <value>The category.</value>
  167. public string Category
  168. {
  169. get { return ScheduledTask.Category; }
  170. }
  171. /// <summary>
  172. /// Gets the current cancellation token
  173. /// </summary>
  174. /// <value>The current cancellation token source.</value>
  175. private CancellationTokenSource CurrentCancellationTokenSource { get; set; }
  176. /// <summary>
  177. /// Gets or sets the current execution start time.
  178. /// </summary>
  179. /// <value>The current execution start time.</value>
  180. private DateTime CurrentExecutionStartTime { get; set; }
  181. /// <summary>
  182. /// Gets the state.
  183. /// </summary>
  184. /// <value>The state.</value>
  185. public TaskState State
  186. {
  187. get
  188. {
  189. if (CurrentCancellationTokenSource != null)
  190. {
  191. return CurrentCancellationTokenSource.IsCancellationRequested
  192. ? TaskState.Cancelling
  193. : TaskState.Running;
  194. }
  195. return TaskState.Idle;
  196. }
  197. }
  198. /// <summary>
  199. /// Gets the current progress.
  200. /// </summary>
  201. /// <value>The current progress.</value>
  202. public double? CurrentProgress { get; private set; }
  203. /// <summary>
  204. /// The _triggers
  205. /// </summary>
  206. private IEnumerable<ITaskTrigger> _triggers;
  207. /// <summary>
  208. /// The _triggers initialized
  209. /// </summary>
  210. private bool _triggersInitialized;
  211. /// <summary>
  212. /// The _triggers sync lock
  213. /// </summary>
  214. private object _triggersSyncLock = new object();
  215. /// <summary>
  216. /// Gets the triggers that define when the task will run
  217. /// </summary>
  218. /// <value>The triggers.</value>
  219. /// <exception cref="System.ArgumentNullException">value</exception>
  220. public IEnumerable<ITaskTrigger> Triggers
  221. {
  222. get
  223. {
  224. LazyInitializer.EnsureInitialized(ref _triggers, ref _triggersInitialized, ref _triggersSyncLock, LoadTriggers);
  225. return _triggers;
  226. }
  227. set
  228. {
  229. if (value == null)
  230. {
  231. throw new ArgumentNullException("value");
  232. }
  233. // Cleanup current triggers
  234. if (_triggers != null)
  235. {
  236. DisposeTriggers();
  237. }
  238. _triggers = value.ToList();
  239. _triggersInitialized = true;
  240. ReloadTriggerEvents(false);
  241. SaveTriggers(_triggers);
  242. }
  243. }
  244. /// <summary>
  245. /// The _id
  246. /// </summary>
  247. private string _id;
  248. /// <summary>
  249. /// Gets the unique id.
  250. /// </summary>
  251. /// <value>The unique id.</value>
  252. public string Id
  253. {
  254. get
  255. {
  256. if (_id == null)
  257. {
  258. _id = ScheduledTask.GetType().FullName.GetMD5().ToString("N");
  259. }
  260. return _id;
  261. }
  262. }
  263. /// <summary>
  264. /// Reloads the trigger events.
  265. /// </summary>
  266. /// <param name="isApplicationStartup">if set to <c>true</c> [is application startup].</param>
  267. private void ReloadTriggerEvents(bool isApplicationStartup)
  268. {
  269. foreach (var trigger in Triggers)
  270. {
  271. trigger.Stop();
  272. trigger.Triggered -= trigger_Triggered;
  273. trigger.Triggered += trigger_Triggered;
  274. trigger.Start(isApplicationStartup);
  275. }
  276. }
  277. /// <summary>
  278. /// Handles the Triggered event of the trigger control.
  279. /// </summary>
  280. /// <param name="sender">The source of the event.</param>
  281. /// <param name="e">The <see cref="EventArgs" /> instance containing the event data.</param>
  282. async void trigger_Triggered(object sender, EventArgs e)
  283. {
  284. var trigger = (ITaskTrigger)sender;
  285. var configurableTask = ScheduledTask as IConfigurableScheduledTask;
  286. if (configurableTask != null && !configurableTask.IsEnabled)
  287. {
  288. return;
  289. }
  290. Logger.Info("{0} fired for task: {1}", trigger.GetType().Name, Name);
  291. trigger.Stop();
  292. TaskManager.QueueScheduledTask(ScheduledTask);
  293. await Task.Delay(1000).ConfigureAwait(false);
  294. trigger.Start(false);
  295. }
  296. /// <summary>
  297. /// Executes the task
  298. /// </summary>
  299. /// <returns>Task.</returns>
  300. /// <exception cref="System.InvalidOperationException">Cannot execute a Task that is already running</exception>
  301. public async Task Execute()
  302. {
  303. // Cancel the current execution, if any
  304. if (CurrentCancellationTokenSource != null)
  305. {
  306. throw new InvalidOperationException("Cannot execute a Task that is already running");
  307. }
  308. var progress = new Progress<double>();
  309. CurrentCancellationTokenSource = new CancellationTokenSource();
  310. Logger.Info("Executing {0}", Name);
  311. ((TaskManager)TaskManager).OnTaskExecuting(this);
  312. progress.ProgressChanged += progress_ProgressChanged;
  313. TaskCompletionStatus status;
  314. CurrentExecutionStartTime = DateTime.UtcNow;
  315. Exception failureException = null;
  316. try
  317. {
  318. await ExecuteTask(CurrentCancellationTokenSource.Token, progress).ConfigureAwait(false);
  319. status = TaskCompletionStatus.Completed;
  320. }
  321. catch (OperationCanceledException)
  322. {
  323. status = TaskCompletionStatus.Cancelled;
  324. }
  325. catch (Exception ex)
  326. {
  327. Logger.ErrorException("Error", ex);
  328. failureException = ex;
  329. status = TaskCompletionStatus.Failed;
  330. }
  331. var startTime = CurrentExecutionStartTime;
  332. var endTime = DateTime.UtcNow;
  333. progress.ProgressChanged -= progress_ProgressChanged;
  334. CurrentCancellationTokenSource.Dispose();
  335. CurrentCancellationTokenSource = null;
  336. CurrentProgress = null;
  337. OnTaskCompleted(startTime, endTime, status, failureException);
  338. // Bad practice, i know. But we keep a lot in memory, unfortunately.
  339. GC.Collect(2, GCCollectionMode.Forced, true);
  340. GC.Collect(2, GCCollectionMode.Forced, true);
  341. }
  342. /// <summary>
  343. /// Executes the task.
  344. /// </summary>
  345. /// <param name="cancellationToken">The cancellation token.</param>
  346. /// <param name="progress">The progress.</param>
  347. /// <returns>Task.</returns>
  348. private Task ExecuteTask(CancellationToken cancellationToken, IProgress<double> progress)
  349. {
  350. return Task.Run(async () => await ScheduledTask.Execute(cancellationToken, progress).ConfigureAwait(false), cancellationToken);
  351. }
  352. /// <summary>
  353. /// Progress_s the progress changed.
  354. /// </summary>
  355. /// <param name="sender">The sender.</param>
  356. /// <param name="e">The e.</param>
  357. void progress_ProgressChanged(object sender, double e)
  358. {
  359. CurrentProgress = e;
  360. EventHelper.FireEventIfNotNull(TaskProgress, this, new GenericEventArgs<double>
  361. {
  362. Argument = e
  363. }, Logger);
  364. }
  365. /// <summary>
  366. /// Stops the task if it is currently executing
  367. /// </summary>
  368. /// <exception cref="System.InvalidOperationException">Cannot cancel a Task unless it is in the Running state.</exception>
  369. public void Cancel()
  370. {
  371. if (State != TaskState.Running)
  372. {
  373. throw new InvalidOperationException("Cannot cancel a Task unless it is in the Running state.");
  374. }
  375. CancelIfRunning();
  376. }
  377. /// <summary>
  378. /// Cancels if running.
  379. /// </summary>
  380. public void CancelIfRunning()
  381. {
  382. if (State == TaskState.Running)
  383. {
  384. Logger.Info("Attempting to cancel Scheduled Task {0}", Name);
  385. CurrentCancellationTokenSource.Cancel();
  386. }
  387. }
  388. /// <summary>
  389. /// Gets the scheduled tasks configuration directory.
  390. /// </summary>
  391. /// <returns>System.String.</returns>
  392. private string GetScheduledTasksConfigurationDirectory()
  393. {
  394. return Path.Combine(ApplicationPaths.ConfigurationDirectoryPath, "ScheduledTasks");
  395. }
  396. /// <summary>
  397. /// Gets the scheduled tasks data directory.
  398. /// </summary>
  399. /// <returns>System.String.</returns>
  400. private string GetScheduledTasksDataDirectory()
  401. {
  402. return Path.Combine(ApplicationPaths.DataPath, "ScheduledTasks");
  403. }
  404. /// <summary>
  405. /// Gets the history file path.
  406. /// </summary>
  407. /// <value>The history file path.</value>
  408. private string GetHistoryFilePath()
  409. {
  410. return Path.Combine(GetScheduledTasksDataDirectory(), new Guid(Id) + ".js");
  411. }
  412. /// <summary>
  413. /// Gets the configuration file path.
  414. /// </summary>
  415. /// <returns>System.String.</returns>
  416. private string GetConfigurationFilePath()
  417. {
  418. return Path.Combine(GetScheduledTasksConfigurationDirectory(), new Guid(Id) + ".js");
  419. }
  420. /// <summary>
  421. /// Loads the triggers.
  422. /// </summary>
  423. /// <returns>IEnumerable{BaseTaskTrigger}.</returns>
  424. private IEnumerable<ITaskTrigger> LoadTriggers()
  425. {
  426. try
  427. {
  428. return JsonSerializer.DeserializeFromFile<IEnumerable<TaskTriggerInfo>>(GetConfigurationFilePath())
  429. .Select(ScheduledTaskHelpers.GetTrigger)
  430. .ToList();
  431. }
  432. catch (FileNotFoundException)
  433. {
  434. // File doesn't exist. No biggie. Return defaults.
  435. return ScheduledTask.GetDefaultTriggers();
  436. }
  437. catch (DirectoryNotFoundException)
  438. {
  439. // File doesn't exist. No biggie. Return defaults.
  440. return ScheduledTask.GetDefaultTriggers();
  441. }
  442. }
  443. /// <summary>
  444. /// Saves the triggers.
  445. /// </summary>
  446. /// <param name="triggers">The triggers.</param>
  447. private void SaveTriggers(IEnumerable<ITaskTrigger> triggers)
  448. {
  449. var path = GetConfigurationFilePath();
  450. Directory.CreateDirectory(Path.GetDirectoryName(path));
  451. JsonSerializer.SerializeToFile(triggers.Select(ScheduledTaskHelpers.GetTriggerInfo), path);
  452. }
  453. /// <summary>
  454. /// Called when [task completed].
  455. /// </summary>
  456. /// <param name="startTime">The start time.</param>
  457. /// <param name="endTime">The end time.</param>
  458. /// <param name="status">The status.</param>
  459. private void OnTaskCompleted(DateTime startTime, DateTime endTime, TaskCompletionStatus status, Exception ex)
  460. {
  461. var elapsedTime = endTime - startTime;
  462. Logger.Info("{0} {1} after {2} minute(s) and {3} seconds", Name, status, Math.Truncate(elapsedTime.TotalMinutes), elapsedTime.Seconds);
  463. var result = new TaskResult
  464. {
  465. StartTimeUtc = startTime,
  466. EndTimeUtc = endTime,
  467. Status = status,
  468. Name = Name,
  469. Id = Id
  470. };
  471. if (ex != null)
  472. {
  473. result.ErrorMessage = ex.Message;
  474. }
  475. var path = GetHistoryFilePath();
  476. Directory.CreateDirectory(Path.GetDirectoryName(path));
  477. JsonSerializer.SerializeToFile(result, path);
  478. LastExecutionResult = result;
  479. ((TaskManager)TaskManager).OnTaskCompleted(this, result);
  480. }
  481. /// <summary>
  482. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  483. /// </summary>
  484. public void Dispose()
  485. {
  486. Dispose(true);
  487. GC.SuppressFinalize(this);
  488. }
  489. /// <summary>
  490. /// Releases unmanaged and - optionally - managed resources.
  491. /// </summary>
  492. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  493. protected virtual void Dispose(bool dispose)
  494. {
  495. if (dispose)
  496. {
  497. DisposeTriggers();
  498. if (State == TaskState.Running)
  499. {
  500. OnTaskCompleted(CurrentExecutionStartTime, DateTime.UtcNow, TaskCompletionStatus.Aborted, null);
  501. }
  502. if (CurrentCancellationTokenSource != null)
  503. {
  504. CurrentCancellationTokenSource.Dispose();
  505. }
  506. }
  507. }
  508. /// <summary>
  509. /// Disposes each trigger
  510. /// </summary>
  511. private void DisposeTriggers()
  512. {
  513. foreach (var trigger in Triggers)
  514. {
  515. trigger.Triggered -= trigger_Triggered;
  516. trigger.Stop();
  517. }
  518. }
  519. }
  520. }