LibraryMonitor.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762
  1. using MediaBrowser.Common.ScheduledTasks;
  2. using MediaBrowser.Controller.Configuration;
  3. using MediaBrowser.Controller.Entities;
  4. using MediaBrowser.Controller.Library;
  5. using MediaBrowser.Controller.Plugins;
  6. using MediaBrowser.Model.Configuration;
  7. using MediaBrowser.Model.Logging;
  8. using MediaBrowser.Server.Implementations.ScheduledTasks;
  9. using Microsoft.Win32;
  10. using System;
  11. using System.Collections.Concurrent;
  12. using System.Collections.Generic;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. using CommonIO;
  18. using MediaBrowser.Controller;
  19. namespace MediaBrowser.Server.Implementations.IO
  20. {
  21. public class LibraryMonitor : ILibraryMonitor
  22. {
  23. /// <summary>
  24. /// The file system watchers
  25. /// </summary>
  26. private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
  27. /// <summary>
  28. /// The update timer
  29. /// </summary>
  30. private Timer _updateTimer;
  31. /// <summary>
  32. /// The affected paths
  33. /// </summary>
  34. private readonly ConcurrentDictionary<string, string> _affectedPaths = new ConcurrentDictionary<string, string>();
  35. /// <summary>
  36. /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications.
  37. /// </summary>
  38. private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  39. /// <summary>
  40. /// Any file name ending in any of these will be ignored by the watchers
  41. /// </summary>
  42. private readonly IReadOnlyList<string> _alwaysIgnoreFiles = new List<string>
  43. {
  44. "thumbs.db",
  45. "small.jpg",
  46. "albumart.jpg",
  47. // WMC temp recording directories that will constantly be written to
  48. "TempRec",
  49. "TempSBE"
  50. };
  51. /// <summary>
  52. /// The timer lock
  53. /// </summary>
  54. private readonly object _timerLock = new object();
  55. /// <summary>
  56. /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
  57. /// </summary>
  58. /// <param name="path">The path.</param>
  59. private void TemporarilyIgnore(string path)
  60. {
  61. _tempIgnoredPaths[path] = path;
  62. }
  63. public void ReportFileSystemChangeBeginning(string path)
  64. {
  65. if (string.IsNullOrEmpty(path))
  66. {
  67. throw new ArgumentNullException("path");
  68. }
  69. TemporarilyIgnore(path);
  70. }
  71. public bool IsPathLocked(string path)
  72. {
  73. var lockedPaths = _tempIgnoredPaths.Keys.ToList();
  74. return lockedPaths.Any(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase) || _fileSystem.ContainsSubPath(i, path));
  75. }
  76. public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
  77. {
  78. if (string.IsNullOrEmpty(path))
  79. {
  80. throw new ArgumentNullException("path");
  81. }
  82. // This is an arbitraty amount of time, but delay it because file system writes often trigger events long after the file was actually written to.
  83. // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
  84. // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
  85. await Task.Delay(25000).ConfigureAwait(false);
  86. string val;
  87. _tempIgnoredPaths.TryRemove(path, out val);
  88. if (refreshPath)
  89. {
  90. ReportFileSystemChanged(path);
  91. }
  92. }
  93. /// <summary>
  94. /// Gets or sets the logger.
  95. /// </summary>
  96. /// <value>The logger.</value>
  97. private ILogger Logger { get; set; }
  98. /// <summary>
  99. /// Gets or sets the task manager.
  100. /// </summary>
  101. /// <value>The task manager.</value>
  102. private ITaskManager TaskManager { get; set; }
  103. private ILibraryManager LibraryManager { get; set; }
  104. private IServerConfigurationManager ConfigurationManager { get; set; }
  105. private readonly IFileSystem _fileSystem;
  106. private readonly IServerApplicationHost _appHost;
  107. /// <summary>
  108. /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
  109. /// </summary>
  110. public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IServerApplicationHost appHost)
  111. {
  112. if (taskManager == null)
  113. {
  114. throw new ArgumentNullException("taskManager");
  115. }
  116. LibraryManager = libraryManager;
  117. TaskManager = taskManager;
  118. Logger = logManager.GetLogger(GetType().Name);
  119. ConfigurationManager = configurationManager;
  120. _fileSystem = fileSystem;
  121. _appHost = appHost;
  122. SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
  123. }
  124. /// <summary>
  125. /// Handles the PowerModeChanged event of the SystemEvents control.
  126. /// </summary>
  127. /// <param name="sender">The source of the event.</param>
  128. /// <param name="e">The <see cref="PowerModeChangedEventArgs"/> instance containing the event data.</param>
  129. void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
  130. {
  131. Restart();
  132. }
  133. private void Restart()
  134. {
  135. Stop();
  136. Start();
  137. }
  138. private bool EnableLibraryMonitor
  139. {
  140. get
  141. {
  142. switch (ConfigurationManager.Configuration.EnableLibraryMonitor)
  143. {
  144. case AutoOnOff.Auto:
  145. return Environment.OSVersion.Platform == PlatformID.Win32NT;
  146. case AutoOnOff.Enabled:
  147. return true;
  148. default:
  149. return false;
  150. }
  151. }
  152. }
  153. public void Start()
  154. {
  155. if (EnableLibraryMonitor)
  156. {
  157. StartInternal();
  158. }
  159. }
  160. /// <summary>
  161. /// Starts this instance.
  162. /// </summary>
  163. private void StartInternal()
  164. {
  165. LibraryManager.ItemAdded += LibraryManager_ItemAdded;
  166. LibraryManager.ItemRemoved += LibraryManager_ItemRemoved;
  167. var pathsToWatch = new List<string> { LibraryManager.RootFolder.Path };
  168. var paths = LibraryManager
  169. .RootFolder
  170. .Children
  171. .OfType<Folder>()
  172. .SelectMany(f => f.PhysicalLocations)
  173. .Distinct(StringComparer.OrdinalIgnoreCase)
  174. .OrderBy(i => i)
  175. .ToList();
  176. foreach (var path in paths)
  177. {
  178. if (!ContainsParentFolder(pathsToWatch, path))
  179. {
  180. pathsToWatch.Add(path);
  181. }
  182. }
  183. foreach (var path in pathsToWatch)
  184. {
  185. StartWatchingPath(path);
  186. }
  187. }
  188. /// <summary>
  189. /// Handles the ItemRemoved event of the LibraryManager control.
  190. /// </summary>
  191. /// <param name="sender">The source of the event.</param>
  192. /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
  193. void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
  194. {
  195. if (e.Item.GetParent() is AggregateFolder)
  196. {
  197. StopWatchingPath(e.Item.Path);
  198. }
  199. }
  200. /// <summary>
  201. /// Handles the ItemAdded event of the LibraryManager control.
  202. /// </summary>
  203. /// <param name="sender">The source of the event.</param>
  204. /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
  205. void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
  206. {
  207. if (e.Item.GetParent() is AggregateFolder)
  208. {
  209. StartWatchingPath(e.Item.Path);
  210. }
  211. }
  212. /// <summary>
  213. /// Examine a list of strings assumed to be file paths to see if it contains a parent of
  214. /// the provided path.
  215. /// </summary>
  216. /// <param name="lst">The LST.</param>
  217. /// <param name="path">The path.</param>
  218. /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
  219. /// <exception cref="System.ArgumentNullException">path</exception>
  220. private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
  221. {
  222. if (string.IsNullOrWhiteSpace(path))
  223. {
  224. throw new ArgumentNullException("path");
  225. }
  226. path = path.TrimEnd(Path.DirectorySeparatorChar);
  227. return lst.Any(str =>
  228. {
  229. //this should be a little quicker than examining each actual parent folder...
  230. var compare = str.TrimEnd(Path.DirectorySeparatorChar);
  231. return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
  232. });
  233. }
  234. /// <summary>
  235. /// Starts the watching path.
  236. /// </summary>
  237. /// <param name="path">The path.</param>
  238. private void StartWatchingPath(string path)
  239. {
  240. // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel
  241. Task.Run(() =>
  242. {
  243. try
  244. {
  245. var newWatcher = new FileSystemWatcher(path, "*")
  246. {
  247. IncludeSubdirectories = true
  248. };
  249. if (Environment.OSVersion.Platform == PlatformID.Win32NT)
  250. {
  251. newWatcher.InternalBufferSize = 32767;
  252. }
  253. newWatcher.NotifyFilter = NotifyFilters.CreationTime |
  254. NotifyFilters.DirectoryName |
  255. NotifyFilters.FileName |
  256. NotifyFilters.LastWrite |
  257. NotifyFilters.Size |
  258. NotifyFilters.Attributes;
  259. newWatcher.Created += watcher_Changed;
  260. newWatcher.Deleted += watcher_Changed;
  261. newWatcher.Renamed += watcher_Changed;
  262. newWatcher.Changed += watcher_Changed;
  263. newWatcher.Error += watcher_Error;
  264. if (_fileSystemWatchers.TryAdd(path, newWatcher))
  265. {
  266. newWatcher.EnableRaisingEvents = true;
  267. Logger.Info("Watching directory " + path);
  268. }
  269. else
  270. {
  271. Logger.Info("Unable to add directory watcher for {0}. It already exists in the dictionary.", path);
  272. newWatcher.Dispose();
  273. }
  274. }
  275. catch (Exception ex)
  276. {
  277. Logger.ErrorException("Error watching path: {0}", ex, path);
  278. }
  279. });
  280. }
  281. /// <summary>
  282. /// Stops the watching path.
  283. /// </summary>
  284. /// <param name="path">The path.</param>
  285. private void StopWatchingPath(string path)
  286. {
  287. FileSystemWatcher watcher;
  288. if (_fileSystemWatchers.TryGetValue(path, out watcher))
  289. {
  290. DisposeWatcher(watcher);
  291. }
  292. }
  293. /// <summary>
  294. /// Disposes the watcher.
  295. /// </summary>
  296. /// <param name="watcher">The watcher.</param>
  297. private void DisposeWatcher(FileSystemWatcher watcher)
  298. {
  299. try
  300. {
  301. using (watcher)
  302. {
  303. Logger.Info("Stopping directory watching for path {0}", watcher.Path);
  304. watcher.EnableRaisingEvents = false;
  305. }
  306. }
  307. catch
  308. {
  309. }
  310. finally
  311. {
  312. RemoveWatcherFromList(watcher);
  313. }
  314. }
  315. /// <summary>
  316. /// Removes the watcher from list.
  317. /// </summary>
  318. /// <param name="watcher">The watcher.</param>
  319. private void RemoveWatcherFromList(FileSystemWatcher watcher)
  320. {
  321. FileSystemWatcher removed;
  322. _fileSystemWatchers.TryRemove(watcher.Path, out removed);
  323. }
  324. /// <summary>
  325. /// Handles the Error event of the watcher control.
  326. /// </summary>
  327. /// <param name="sender">The source of the event.</param>
  328. /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
  329. void watcher_Error(object sender, ErrorEventArgs e)
  330. {
  331. var ex = e.GetException();
  332. var dw = (FileSystemWatcher)sender;
  333. Logger.ErrorException("Error in Directory watcher for: " + dw.Path, ex);
  334. DisposeWatcher(dw);
  335. if (ConfigurationManager.Configuration.EnableLibraryMonitor == AutoOnOff.Auto)
  336. {
  337. Logger.Info("Disabling realtime monitor to prevent future instability");
  338. ConfigurationManager.Configuration.EnableLibraryMonitor = AutoOnOff.Disabled;
  339. Stop();
  340. }
  341. }
  342. /// <summary>
  343. /// Handles the Changed event of the watcher control.
  344. /// </summary>
  345. /// <param name="sender">The source of the event.</param>
  346. /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
  347. void watcher_Changed(object sender, FileSystemEventArgs e)
  348. {
  349. try
  350. {
  351. Logger.Debug("Changed detected of type " + e.ChangeType + " to " + e.FullPath);
  352. ReportFileSystemChanged(e.FullPath);
  353. }
  354. catch (Exception ex)
  355. {
  356. Logger.ErrorException("Exception in ReportFileSystemChanged. Path: {0}", ex, e.FullPath);
  357. }
  358. }
  359. public void ReportFileSystemChanged(string path)
  360. {
  361. if (string.IsNullOrEmpty(path))
  362. {
  363. throw new ArgumentNullException("path");
  364. }
  365. var filename = Path.GetFileName(path);
  366. var monitorPath = !(!string.IsNullOrEmpty(filename) && _alwaysIgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase));
  367. // Ignore certain files
  368. var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList();
  369. // If the parent of an ignored path has a change event, ignore that too
  370. if (tempIgnorePaths.Any(i =>
  371. {
  372. if (string.Equals(i, path, StringComparison.OrdinalIgnoreCase))
  373. {
  374. Logger.Debug("Ignoring change to {0}", path);
  375. return true;
  376. }
  377. if (_fileSystem.ContainsSubPath(i, path))
  378. {
  379. Logger.Debug("Ignoring change to {0}", path);
  380. return true;
  381. }
  382. // Go up a level
  383. var parent = Path.GetDirectoryName(i);
  384. if (!string.IsNullOrEmpty(parent))
  385. {
  386. if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase))
  387. {
  388. Logger.Debug("Ignoring change to {0}", path);
  389. return true;
  390. }
  391. }
  392. return false;
  393. }))
  394. {
  395. monitorPath = false;
  396. }
  397. if (monitorPath)
  398. {
  399. // Avoid implicitly captured closure
  400. var affectedPath = path;
  401. _affectedPaths.AddOrUpdate(path, path, (key, oldValue) => affectedPath);
  402. }
  403. RestartTimer();
  404. }
  405. private void RestartTimer()
  406. {
  407. lock (_timerLock)
  408. {
  409. if (_updateTimer == null)
  410. {
  411. _updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
  412. }
  413. else
  414. {
  415. _updateTimer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.LibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
  416. }
  417. }
  418. }
  419. /// <summary>
  420. /// Timers the stopped.
  421. /// </summary>
  422. /// <param name="stateInfo">The state info.</param>
  423. private async void TimerStopped(object stateInfo)
  424. {
  425. // Extend the timer as long as any of the paths are still being written to.
  426. if (_affectedPaths.Any(p => IsFileLocked(p.Key)))
  427. {
  428. Logger.Info("Timer extended.");
  429. RestartTimer();
  430. return;
  431. }
  432. Logger.Debug("Timer stopped.");
  433. DisposeTimer();
  434. var paths = _affectedPaths.Keys.ToList();
  435. _affectedPaths.Clear();
  436. try
  437. {
  438. await ProcessPathChanges(paths).ConfigureAwait(false);
  439. }
  440. catch (Exception ex)
  441. {
  442. Logger.ErrorException("Error processing directory changes", ex);
  443. }
  444. }
  445. private bool IsFileLocked(string path)
  446. {
  447. if (Environment.OSVersion.Platform != PlatformID.Win32NT)
  448. {
  449. // Causing lockups on linux
  450. return false;
  451. }
  452. try
  453. {
  454. var data = _fileSystem.GetFileSystemInfo(path);
  455. if (!data.Exists
  456. || data.IsDirectory
  457. // Opening a writable stream will fail with readonly files
  458. || data.Attributes.HasFlag(FileAttributes.ReadOnly))
  459. {
  460. return false;
  461. }
  462. }
  463. catch (IOException)
  464. {
  465. return false;
  466. }
  467. catch (Exception ex)
  468. {
  469. Logger.ErrorException("Error getting file system info for: {0}", ex, path);
  470. return false;
  471. }
  472. // In order to determine if the file is being written to, we have to request write access
  473. // But if the server only has readonly access, this is going to cause this entire algorithm to fail
  474. // So we'll take a best guess about our access level
  475. var requestedFileAccess = ConfigurationManager.Configuration.SaveLocalMeta
  476. ? FileAccess.ReadWrite
  477. : FileAccess.Read;
  478. try
  479. {
  480. using (_fileSystem.GetFileStream(path, FileMode.Open, requestedFileAccess, FileShare.ReadWrite))
  481. {
  482. if (_updateTimer != null)
  483. {
  484. //file is not locked
  485. return false;
  486. }
  487. }
  488. }
  489. catch (DirectoryNotFoundException)
  490. {
  491. // File may have been deleted
  492. return false;
  493. }
  494. catch (FileNotFoundException)
  495. {
  496. // File may have been deleted
  497. return false;
  498. }
  499. catch (IOException)
  500. {
  501. //the file is unavailable because it is:
  502. //still being written to
  503. //or being processed by another thread
  504. //or does not exist (has already been processed)
  505. Logger.Debug("{0} is locked.", path);
  506. return true;
  507. }
  508. catch (Exception ex)
  509. {
  510. Logger.ErrorException("Error determining if file is locked: {0}", ex, path);
  511. return false;
  512. }
  513. return false;
  514. }
  515. private void DisposeTimer()
  516. {
  517. lock (_timerLock)
  518. {
  519. if (_updateTimer != null)
  520. {
  521. _updateTimer.Dispose();
  522. _updateTimer = null;
  523. }
  524. }
  525. }
  526. /// <summary>
  527. /// Processes the path changes.
  528. /// </summary>
  529. /// <param name="paths">The paths.</param>
  530. /// <returns>Task.</returns>
  531. private async Task ProcessPathChanges(List<string> paths)
  532. {
  533. var itemsToRefresh = paths
  534. .Select(GetAffectedBaseItem)
  535. .Where(item => item != null)
  536. .Distinct()
  537. .ToList();
  538. foreach (var p in paths)
  539. {
  540. Logger.Info(p + " reports change.");
  541. }
  542. // If the root folder changed, run the library task so the user can see it
  543. if (itemsToRefresh.Any(i => i is AggregateFolder))
  544. {
  545. TaskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
  546. return;
  547. }
  548. foreach (var item in itemsToRefresh)
  549. {
  550. Logger.Info(item.Name + " (" + item.Path + ") will be refreshed.");
  551. try
  552. {
  553. await item.ChangedExternally().ConfigureAwait(false);
  554. }
  555. catch (IOException ex)
  556. {
  557. // For now swallow and log.
  558. // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
  559. // Should we remove it from it's parent?
  560. Logger.ErrorException("Error refreshing {0}", ex, item.Name);
  561. }
  562. catch (Exception ex)
  563. {
  564. Logger.ErrorException("Error refreshing {0}", ex, item.Name);
  565. }
  566. }
  567. }
  568. /// <summary>
  569. /// Gets the affected base item.
  570. /// </summary>
  571. /// <param name="path">The path.</param>
  572. /// <returns>BaseItem.</returns>
  573. private BaseItem GetAffectedBaseItem(string path)
  574. {
  575. BaseItem item = null;
  576. while (item == null && !string.IsNullOrEmpty(path))
  577. {
  578. item = LibraryManager.FindByPath(path, null);
  579. path = Path.GetDirectoryName(path);
  580. }
  581. if (item != null)
  582. {
  583. // If the item has been deleted find the first valid parent that still exists
  584. while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path))
  585. {
  586. item = item.GetParent();
  587. if (item == null)
  588. {
  589. break;
  590. }
  591. }
  592. }
  593. return item;
  594. }
  595. /// <summary>
  596. /// Stops this instance.
  597. /// </summary>
  598. public void Stop()
  599. {
  600. LibraryManager.ItemAdded -= LibraryManager_ItemAdded;
  601. LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved;
  602. foreach (var watcher in _fileSystemWatchers.Values.ToList())
  603. {
  604. watcher.Created -= watcher_Changed;
  605. watcher.Deleted -= watcher_Changed;
  606. watcher.Renamed -= watcher_Changed;
  607. watcher.Changed -= watcher_Changed;
  608. try
  609. {
  610. watcher.EnableRaisingEvents = false;
  611. }
  612. catch (InvalidOperationException)
  613. {
  614. // Seeing this under mono on linux sometimes
  615. // Collection was modified; enumeration operation may not execute.
  616. }
  617. watcher.Dispose();
  618. }
  619. DisposeTimer();
  620. _fileSystemWatchers.Clear();
  621. _affectedPaths.Clear();
  622. }
  623. /// <summary>
  624. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  625. /// </summary>
  626. public void Dispose()
  627. {
  628. Dispose(true);
  629. GC.SuppressFinalize(this);
  630. }
  631. /// <summary>
  632. /// Releases unmanaged and - optionally - managed resources.
  633. /// </summary>
  634. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  635. protected virtual void Dispose(bool dispose)
  636. {
  637. if (dispose)
  638. {
  639. Stop();
  640. }
  641. }
  642. }
  643. public class LibraryMonitorStartup : IServerEntryPoint
  644. {
  645. private readonly ILibraryMonitor _monitor;
  646. public LibraryMonitorStartup(ILibraryMonitor monitor)
  647. {
  648. _monitor = monitor;
  649. }
  650. public void Run()
  651. {
  652. _monitor.Start();
  653. }
  654. public void Dispose()
  655. {
  656. }
  657. }
  658. }