2
0

LibraryMonitor.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732
  1. using MediaBrowser.Common.IO;
  2. using MediaBrowser.Common.ScheduledTasks;
  3. using MediaBrowser.Controller.Configuration;
  4. using MediaBrowser.Controller.Entities;
  5. using MediaBrowser.Controller.Library;
  6. using MediaBrowser.Controller.Plugins;
  7. using MediaBrowser.Model.Configuration;
  8. using MediaBrowser.Model.Logging;
  9. using MediaBrowser.Server.Implementations.ScheduledTasks;
  10. using Microsoft.Win32;
  11. using System;
  12. using System.Collections.Concurrent;
  13. using System.Collections.Generic;
  14. using System.IO;
  15. using System.Linq;
  16. using System.Threading;
  17. using System.Threading.Tasks;
  18. using CommonIO;
  19. using MediaBrowser.Controller;
  20. namespace MediaBrowser.Server.Implementations.IO
  21. {
  22. public class LibraryMonitor : ILibraryMonitor
  23. {
  24. /// <summary>
  25. /// The file system watchers
  26. /// </summary>
  27. private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
  28. /// <summary>
  29. /// The update timer
  30. /// </summary>
  31. private Timer _updateTimer;
  32. /// <summary>
  33. /// The affected paths
  34. /// </summary>
  35. private readonly ConcurrentDictionary<string, string> _affectedPaths = new ConcurrentDictionary<string, string>();
  36. /// <summary>
  37. /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications.
  38. /// </summary>
  39. private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  40. /// <summary>
  41. /// Any file name ending in any of these will be ignored by the watchers
  42. /// </summary>
  43. private readonly IReadOnlyList<string> _alwaysIgnoreFiles = new List<string>
  44. {
  45. "thumbs.db",
  46. "small.jpg",
  47. "albumart.jpg",
  48. // WMC temp recording directories that will constantly be written to
  49. "TempRec",
  50. "TempSBE"
  51. };
  52. /// <summary>
  53. /// The timer lock
  54. /// </summary>
  55. private readonly object _timerLock = new object();
  56. /// <summary>
  57. /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
  58. /// </summary>
  59. /// <param name="path">The path.</param>
  60. private void TemporarilyIgnore(string path)
  61. {
  62. _tempIgnoredPaths[path] = path;
  63. }
  64. public void ReportFileSystemChangeBeginning(string path)
  65. {
  66. if (string.IsNullOrEmpty(path))
  67. {
  68. throw new ArgumentNullException("path");
  69. }
  70. TemporarilyIgnore(path);
  71. }
  72. public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
  73. {
  74. if (string.IsNullOrEmpty(path))
  75. {
  76. throw new ArgumentNullException("path");
  77. }
  78. // 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.
  79. // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
  80. // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
  81. await Task.Delay(20000).ConfigureAwait(false);
  82. string val;
  83. _tempIgnoredPaths.TryRemove(path, out val);
  84. if (refreshPath)
  85. {
  86. ReportFileSystemChanged(path);
  87. }
  88. }
  89. /// <summary>
  90. /// Gets or sets the logger.
  91. /// </summary>
  92. /// <value>The logger.</value>
  93. private ILogger Logger { get; set; }
  94. /// <summary>
  95. /// Gets or sets the task manager.
  96. /// </summary>
  97. /// <value>The task manager.</value>
  98. private ITaskManager TaskManager { get; set; }
  99. private ILibraryManager LibraryManager { get; set; }
  100. private IServerConfigurationManager ConfigurationManager { get; set; }
  101. private readonly IFileSystem _fileSystem;
  102. private readonly IServerApplicationHost _appHost;
  103. /// <summary>
  104. /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
  105. /// </summary>
  106. public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IServerApplicationHost appHost)
  107. {
  108. if (taskManager == null)
  109. {
  110. throw new ArgumentNullException("taskManager");
  111. }
  112. LibraryManager = libraryManager;
  113. TaskManager = taskManager;
  114. Logger = logManager.GetLogger(GetType().Name);
  115. ConfigurationManager = configurationManager;
  116. _fileSystem = fileSystem;
  117. _appHost = appHost;
  118. SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
  119. }
  120. /// <summary>
  121. /// Handles the PowerModeChanged event of the SystemEvents control.
  122. /// </summary>
  123. /// <param name="sender">The source of the event.</param>
  124. /// <param name="e">The <see cref="PowerModeChangedEventArgs"/> instance containing the event data.</param>
  125. void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
  126. {
  127. Restart();
  128. }
  129. private void Restart()
  130. {
  131. Stop();
  132. Start();
  133. }
  134. private bool EnableLibraryMonitor
  135. {
  136. get
  137. {
  138. if (!_appHost.SupportsLibraryMonitor)
  139. {
  140. return false;
  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.Parent 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.Parent 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.IsNullOrEmpty(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. InternalBufferSize = 32767
  249. };
  250. newWatcher.NotifyFilter = NotifyFilters.CreationTime |
  251. NotifyFilters.DirectoryName |
  252. NotifyFilters.FileName |
  253. NotifyFilters.LastWrite |
  254. NotifyFilters.Size |
  255. NotifyFilters.Attributes;
  256. newWatcher.Created += watcher_Changed;
  257. newWatcher.Deleted += watcher_Changed;
  258. newWatcher.Renamed += watcher_Changed;
  259. newWatcher.Changed += watcher_Changed;
  260. newWatcher.Error += watcher_Error;
  261. if (_fileSystemWatchers.TryAdd(path, newWatcher))
  262. {
  263. newWatcher.EnableRaisingEvents = true;
  264. Logger.Info("Watching directory " + path);
  265. }
  266. else
  267. {
  268. Logger.Info("Unable to add directory watcher for {0}. It already exists in the dictionary.", path);
  269. newWatcher.Dispose();
  270. }
  271. }
  272. catch (Exception ex)
  273. {
  274. Logger.ErrorException("Error watching path: {0}", ex, path);
  275. }
  276. });
  277. }
  278. /// <summary>
  279. /// Stops the watching path.
  280. /// </summary>
  281. /// <param name="path">The path.</param>
  282. private void StopWatchingPath(string path)
  283. {
  284. FileSystemWatcher watcher;
  285. if (_fileSystemWatchers.TryGetValue(path, out watcher))
  286. {
  287. DisposeWatcher(watcher);
  288. }
  289. }
  290. /// <summary>
  291. /// Disposes the watcher.
  292. /// </summary>
  293. /// <param name="watcher">The watcher.</param>
  294. private void DisposeWatcher(FileSystemWatcher watcher)
  295. {
  296. try
  297. {
  298. using (watcher)
  299. {
  300. Logger.Info("Stopping directory watching for path {0}", watcher.Path);
  301. watcher.EnableRaisingEvents = false;
  302. }
  303. }
  304. catch
  305. {
  306. }
  307. finally
  308. {
  309. RemoveWatcherFromList(watcher);
  310. }
  311. }
  312. /// <summary>
  313. /// Removes the watcher from list.
  314. /// </summary>
  315. /// <param name="watcher">The watcher.</param>
  316. private void RemoveWatcherFromList(FileSystemWatcher watcher)
  317. {
  318. FileSystemWatcher removed;
  319. _fileSystemWatchers.TryRemove(watcher.Path, out removed);
  320. }
  321. /// <summary>
  322. /// Handles the Error event of the watcher control.
  323. /// </summary>
  324. /// <param name="sender">The source of the event.</param>
  325. /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
  326. void watcher_Error(object sender, ErrorEventArgs e)
  327. {
  328. var ex = e.GetException();
  329. var dw = (FileSystemWatcher)sender;
  330. Logger.ErrorException("Error in Directory watcher for: " + dw.Path, ex);
  331. DisposeWatcher(dw);
  332. if (ConfigurationManager.Configuration.EnableLibraryMonitor == AutoOnOff.Auto)
  333. {
  334. Logger.Info("Disabling realtime monitor to prevent future instability");
  335. ConfigurationManager.Configuration.EnableLibraryMonitor = AutoOnOff.Disabled;
  336. Stop();
  337. }
  338. }
  339. /// <summary>
  340. /// Handles the Changed event of the watcher control.
  341. /// </summary>
  342. /// <param name="sender">The source of the event.</param>
  343. /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
  344. void watcher_Changed(object sender, FileSystemEventArgs e)
  345. {
  346. try
  347. {
  348. Logger.Debug("Changed detected of type " + e.ChangeType + " to " + e.FullPath);
  349. ReportFileSystemChanged(e.FullPath);
  350. }
  351. catch (Exception ex)
  352. {
  353. Logger.ErrorException("Exception in ReportFileSystemChanged. Path: {0}", ex, e.FullPath);
  354. }
  355. }
  356. public void ReportFileSystemChanged(string path)
  357. {
  358. if (string.IsNullOrEmpty(path))
  359. {
  360. throw new ArgumentNullException("path");
  361. }
  362. var filename = Path.GetFileName(path);
  363. var monitorPath = !(!string.IsNullOrEmpty(filename) && _alwaysIgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase));
  364. // Ignore certain files
  365. var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList();
  366. // If the parent of an ignored path has a change event, ignore that too
  367. if (tempIgnorePaths.Any(i =>
  368. {
  369. if (string.Equals(i, path, StringComparison.OrdinalIgnoreCase))
  370. {
  371. Logger.Debug("Ignoring change to {0}", path);
  372. return true;
  373. }
  374. if (_fileSystem.ContainsSubPath(i, path))
  375. {
  376. Logger.Debug("Ignoring change to {0}", path);
  377. return true;
  378. }
  379. // Go up a level
  380. var parent = Path.GetDirectoryName(i);
  381. if (!string.IsNullOrEmpty(parent))
  382. {
  383. if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase))
  384. {
  385. Logger.Debug("Ignoring change to {0}", path);
  386. return true;
  387. }
  388. }
  389. return false;
  390. }))
  391. {
  392. monitorPath = false;
  393. }
  394. if (monitorPath)
  395. {
  396. // Avoid implicitly captured closure
  397. var affectedPath = path;
  398. _affectedPaths.AddOrUpdate(path, path, (key, oldValue) => affectedPath);
  399. }
  400. RestartTimer();
  401. }
  402. private void RestartTimer()
  403. {
  404. lock (_timerLock)
  405. {
  406. if (_updateTimer == null)
  407. {
  408. _updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.RealtimeLibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
  409. }
  410. else
  411. {
  412. _updateTimer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.RealtimeLibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));
  413. }
  414. }
  415. }
  416. /// <summary>
  417. /// Timers the stopped.
  418. /// </summary>
  419. /// <param name="stateInfo">The state info.</param>
  420. private async void TimerStopped(object stateInfo)
  421. {
  422. // Extend the timer as long as any of the paths are still being written to.
  423. if (_affectedPaths.Any(p => IsFileLocked(p.Key)))
  424. {
  425. Logger.Info("Timer extended.");
  426. RestartTimer();
  427. return;
  428. }
  429. Logger.Debug("Timer stopped.");
  430. DisposeTimer();
  431. var paths = _affectedPaths.Keys.ToList();
  432. _affectedPaths.Clear();
  433. try
  434. {
  435. await ProcessPathChanges(paths).ConfigureAwait(false);
  436. }
  437. catch (Exception ex)
  438. {
  439. Logger.ErrorException("Error processing directory changes", ex);
  440. }
  441. }
  442. private bool IsFileLocked(string path)
  443. {
  444. try
  445. {
  446. var data = _fileSystem.GetFileSystemInfo(path);
  447. if (!data.Exists
  448. || data.Attributes.HasFlag(FileAttributes.Directory)
  449. // Opening a writable stream will fail with readonly files
  450. || data.Attributes.HasFlag(FileAttributes.ReadOnly))
  451. {
  452. return false;
  453. }
  454. }
  455. catch (IOException)
  456. {
  457. return false;
  458. }
  459. catch (Exception ex)
  460. {
  461. Logger.ErrorException("Error getting file system info for: {0}", ex, path);
  462. return false;
  463. }
  464. try
  465. {
  466. using (_fileSystem.GetFileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
  467. {
  468. if (_updateTimer != null)
  469. {
  470. //file is not locked
  471. return false;
  472. }
  473. }
  474. }
  475. catch (DirectoryNotFoundException)
  476. {
  477. // File may have been deleted
  478. return false;
  479. }
  480. catch (FileNotFoundException)
  481. {
  482. // File may have been deleted
  483. return false;
  484. }
  485. catch (IOException)
  486. {
  487. //the file is unavailable because it is:
  488. //still being written to
  489. //or being processed by another thread
  490. //or does not exist (has already been processed)
  491. Logger.Debug("{0} is locked.", path);
  492. return true;
  493. }
  494. catch (Exception ex)
  495. {
  496. Logger.ErrorException("Error determining if file is locked: {0}", ex, path);
  497. return false;
  498. }
  499. return false;
  500. }
  501. private void DisposeTimer()
  502. {
  503. lock (_timerLock)
  504. {
  505. if (_updateTimer != null)
  506. {
  507. _updateTimer.Dispose();
  508. _updateTimer = null;
  509. }
  510. }
  511. }
  512. /// <summary>
  513. /// Processes the path changes.
  514. /// </summary>
  515. /// <param name="paths">The paths.</param>
  516. /// <returns>Task.</returns>
  517. private async Task ProcessPathChanges(List<string> paths)
  518. {
  519. var itemsToRefresh = paths
  520. .Select(GetAffectedBaseItem)
  521. .Where(item => item != null)
  522. .Distinct()
  523. .ToList();
  524. foreach (var p in paths)
  525. {
  526. Logger.Info(p + " reports change.");
  527. }
  528. // If the root folder changed, run the library task so the user can see it
  529. if (itemsToRefresh.Any(i => i is AggregateFolder))
  530. {
  531. TaskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();
  532. return;
  533. }
  534. foreach (var item in itemsToRefresh)
  535. {
  536. Logger.Info(item.Name + " (" + item.Path + ") will be refreshed.");
  537. try
  538. {
  539. await item.ChangedExternally().ConfigureAwait(false);
  540. }
  541. catch (IOException ex)
  542. {
  543. // For now swallow and log.
  544. // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
  545. // Should we remove it from it's parent?
  546. Logger.ErrorException("Error refreshing {0}", ex, item.Name);
  547. }
  548. catch (Exception ex)
  549. {
  550. Logger.ErrorException("Error refreshing {0}", ex, item.Name);
  551. }
  552. }
  553. }
  554. /// <summary>
  555. /// Gets the affected base item.
  556. /// </summary>
  557. /// <param name="path">The path.</param>
  558. /// <returns>BaseItem.</returns>
  559. private BaseItem GetAffectedBaseItem(string path)
  560. {
  561. BaseItem item = null;
  562. while (item == null && !string.IsNullOrEmpty(path))
  563. {
  564. item = LibraryManager.RootFolder.FindByPath(path);
  565. path = Path.GetDirectoryName(path);
  566. }
  567. if (item != null)
  568. {
  569. // If the item has been deleted find the first valid parent that still exists
  570. while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path))
  571. {
  572. item = item.Parent;
  573. if (item == null)
  574. {
  575. break;
  576. }
  577. }
  578. }
  579. return item;
  580. }
  581. /// <summary>
  582. /// Stops this instance.
  583. /// </summary>
  584. public void Stop()
  585. {
  586. LibraryManager.ItemAdded -= LibraryManager_ItemAdded;
  587. LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved;
  588. foreach (var watcher in _fileSystemWatchers.Values.ToList())
  589. {
  590. watcher.Changed -= watcher_Changed;
  591. watcher.EnableRaisingEvents = false;
  592. watcher.Dispose();
  593. }
  594. DisposeTimer();
  595. _fileSystemWatchers.Clear();
  596. _affectedPaths.Clear();
  597. }
  598. /// <summary>
  599. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  600. /// </summary>
  601. public void Dispose()
  602. {
  603. Dispose(true);
  604. GC.SuppressFinalize(this);
  605. }
  606. /// <summary>
  607. /// Releases unmanaged and - optionally - managed resources.
  608. /// </summary>
  609. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  610. protected virtual void Dispose(bool dispose)
  611. {
  612. if (dispose)
  613. {
  614. Stop();
  615. }
  616. }
  617. }
  618. public class LibraryMonitorStartup : IServerEntryPoint
  619. {
  620. private readonly ILibraryMonitor _monitor;
  621. public LibraryMonitorStartup(ILibraryMonitor monitor)
  622. {
  623. _monitor = monitor;
  624. }
  625. public void Run()
  626. {
  627. _monitor.Start();
  628. }
  629. public void Dispose()
  630. {
  631. }
  632. }
  633. }