LibraryMonitor.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  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 Microsoft.Win32;
  9. using System;
  10. using System.Collections.Concurrent;
  11. using System.Collections.Generic;
  12. using System.IO;
  13. using System.Linq;
  14. using System.Threading.Tasks;
  15. using CommonIO;
  16. using MediaBrowser.Controller;
  17. namespace MediaBrowser.Server.Implementations.IO
  18. {
  19. public class LibraryMonitor : ILibraryMonitor
  20. {
  21. /// <summary>
  22. /// The file system watchers
  23. /// </summary>
  24. private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);
  25. /// <summary>
  26. /// The affected paths
  27. /// </summary>
  28. private readonly List<FileRefresher> _activeRefreshers = new List<FileRefresher>();
  29. /// <summary>
  30. /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications.
  31. /// </summary>
  32. private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  33. /// <summary>
  34. /// Any file name ending in any of these will be ignored by the watchers
  35. /// </summary>
  36. private readonly IReadOnlyList<string> _alwaysIgnoreFiles = new List<string>
  37. {
  38. "thumbs.db",
  39. "small.jpg",
  40. "albumart.jpg",
  41. // WMC temp recording directories that will constantly be written to
  42. "TempRec",
  43. "TempSBE"
  44. };
  45. /// <summary>
  46. /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope.
  47. /// </summary>
  48. /// <param name="path">The path.</param>
  49. private void TemporarilyIgnore(string path)
  50. {
  51. _tempIgnoredPaths[path] = path;
  52. }
  53. public void ReportFileSystemChangeBeginning(string path)
  54. {
  55. if (string.IsNullOrEmpty(path))
  56. {
  57. throw new ArgumentNullException("path");
  58. }
  59. TemporarilyIgnore(path);
  60. }
  61. public bool IsPathLocked(string path)
  62. {
  63. var lockedPaths = _tempIgnoredPaths.Keys.ToList();
  64. return lockedPaths.Any(i => string.Equals(i, path, StringComparison.OrdinalIgnoreCase) || _fileSystem.ContainsSubPath(i, path));
  65. }
  66. public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
  67. {
  68. if (string.IsNullOrEmpty(path))
  69. {
  70. throw new ArgumentNullException("path");
  71. }
  72. // 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.
  73. // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds
  74. // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata
  75. await Task.Delay(45000).ConfigureAwait(false);
  76. string val;
  77. _tempIgnoredPaths.TryRemove(path, out val);
  78. if (refreshPath)
  79. {
  80. ReportFileSystemChanged(path);
  81. }
  82. }
  83. /// <summary>
  84. /// Gets or sets the logger.
  85. /// </summary>
  86. /// <value>The logger.</value>
  87. private ILogger Logger { get; set; }
  88. /// <summary>
  89. /// Gets or sets the task manager.
  90. /// </summary>
  91. /// <value>The task manager.</value>
  92. private ITaskManager TaskManager { get; set; }
  93. private ILibraryManager LibraryManager { get; set; }
  94. private IServerConfigurationManager ConfigurationManager { get; set; }
  95. private readonly IFileSystem _fileSystem;
  96. private readonly IServerApplicationHost _appHost;
  97. /// <summary>
  98. /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
  99. /// </summary>
  100. public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IServerApplicationHost appHost)
  101. {
  102. if (taskManager == null)
  103. {
  104. throw new ArgumentNullException("taskManager");
  105. }
  106. LibraryManager = libraryManager;
  107. TaskManager = taskManager;
  108. Logger = logManager.GetLogger(GetType().Name);
  109. ConfigurationManager = configurationManager;
  110. _fileSystem = fileSystem;
  111. _appHost = appHost;
  112. SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;
  113. }
  114. /// <summary>
  115. /// Handles the PowerModeChanged event of the SystemEvents control.
  116. /// </summary>
  117. /// <param name="sender">The source of the event.</param>
  118. /// <param name="e">The <see cref="PowerModeChangedEventArgs"/> instance containing the event data.</param>
  119. void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)
  120. {
  121. Restart();
  122. }
  123. private void Restart()
  124. {
  125. Stop();
  126. Start();
  127. }
  128. private bool EnableLibraryMonitor
  129. {
  130. get
  131. {
  132. switch (ConfigurationManager.Configuration.EnableLibraryMonitor)
  133. {
  134. case AutoOnOff.Auto:
  135. return Environment.OSVersion.Platform == PlatformID.Win32NT;
  136. case AutoOnOff.Enabled:
  137. return true;
  138. default:
  139. return false;
  140. }
  141. }
  142. }
  143. public void Start()
  144. {
  145. if (EnableLibraryMonitor)
  146. {
  147. StartInternal();
  148. }
  149. }
  150. /// <summary>
  151. /// Starts this instance.
  152. /// </summary>
  153. private void StartInternal()
  154. {
  155. LibraryManager.ItemAdded += LibraryManager_ItemAdded;
  156. LibraryManager.ItemRemoved += LibraryManager_ItemRemoved;
  157. var pathsToWatch = new List<string> { LibraryManager.RootFolder.Path };
  158. var paths = LibraryManager
  159. .RootFolder
  160. .Children
  161. .OfType<Folder>()
  162. .SelectMany(f => f.PhysicalLocations)
  163. .Distinct(StringComparer.OrdinalIgnoreCase)
  164. .OrderBy(i => i)
  165. .ToList();
  166. foreach (var path in paths)
  167. {
  168. if (!ContainsParentFolder(pathsToWatch, path))
  169. {
  170. pathsToWatch.Add(path);
  171. }
  172. }
  173. foreach (var path in pathsToWatch)
  174. {
  175. StartWatchingPath(path);
  176. }
  177. }
  178. /// <summary>
  179. /// Handles the ItemRemoved event of the LibraryManager control.
  180. /// </summary>
  181. /// <param name="sender">The source of the event.</param>
  182. /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
  183. void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)
  184. {
  185. if (e.Item.GetParent() is AggregateFolder)
  186. {
  187. StopWatchingPath(e.Item.Path);
  188. }
  189. }
  190. /// <summary>
  191. /// Handles the ItemAdded event of the LibraryManager control.
  192. /// </summary>
  193. /// <param name="sender">The source of the event.</param>
  194. /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
  195. void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e)
  196. {
  197. if (e.Item.GetParent() is AggregateFolder)
  198. {
  199. StartWatchingPath(e.Item.Path);
  200. }
  201. }
  202. /// <summary>
  203. /// Examine a list of strings assumed to be file paths to see if it contains a parent of
  204. /// the provided path.
  205. /// </summary>
  206. /// <param name="lst">The LST.</param>
  207. /// <param name="path">The path.</param>
  208. /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
  209. /// <exception cref="System.ArgumentNullException">path</exception>
  210. private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
  211. {
  212. if (string.IsNullOrWhiteSpace(path))
  213. {
  214. throw new ArgumentNullException("path");
  215. }
  216. path = path.TrimEnd(Path.DirectorySeparatorChar);
  217. return lst.Any(str =>
  218. {
  219. //this should be a little quicker than examining each actual parent folder...
  220. var compare = str.TrimEnd(Path.DirectorySeparatorChar);
  221. return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
  222. });
  223. }
  224. /// <summary>
  225. /// Starts the watching path.
  226. /// </summary>
  227. /// <param name="path">The path.</param>
  228. private void StartWatchingPath(string path)
  229. {
  230. // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel
  231. Task.Run(() =>
  232. {
  233. try
  234. {
  235. var newWatcher = new FileSystemWatcher(path, "*")
  236. {
  237. IncludeSubdirectories = true
  238. };
  239. if (Environment.OSVersion.Platform == PlatformID.Win32NT)
  240. {
  241. newWatcher.InternalBufferSize = 32767;
  242. }
  243. newWatcher.NotifyFilter = NotifyFilters.CreationTime |
  244. NotifyFilters.DirectoryName |
  245. NotifyFilters.FileName |
  246. NotifyFilters.LastWrite |
  247. NotifyFilters.Size |
  248. NotifyFilters.Attributes;
  249. newWatcher.Created += watcher_Changed;
  250. newWatcher.Deleted += watcher_Changed;
  251. newWatcher.Renamed += watcher_Changed;
  252. newWatcher.Changed += watcher_Changed;
  253. newWatcher.Error += watcher_Error;
  254. if (_fileSystemWatchers.TryAdd(path, newWatcher))
  255. {
  256. newWatcher.EnableRaisingEvents = true;
  257. Logger.Info("Watching directory " + path);
  258. }
  259. else
  260. {
  261. Logger.Info("Unable to add directory watcher for {0}. It already exists in the dictionary.", path);
  262. newWatcher.Dispose();
  263. }
  264. }
  265. catch (Exception ex)
  266. {
  267. Logger.ErrorException("Error watching path: {0}", ex, path);
  268. }
  269. });
  270. }
  271. /// <summary>
  272. /// Stops the watching path.
  273. /// </summary>
  274. /// <param name="path">The path.</param>
  275. private void StopWatchingPath(string path)
  276. {
  277. FileSystemWatcher watcher;
  278. if (_fileSystemWatchers.TryGetValue(path, out watcher))
  279. {
  280. DisposeWatcher(watcher);
  281. }
  282. }
  283. /// <summary>
  284. /// Disposes the watcher.
  285. /// </summary>
  286. /// <param name="watcher">The watcher.</param>
  287. private void DisposeWatcher(FileSystemWatcher watcher)
  288. {
  289. try
  290. {
  291. using (watcher)
  292. {
  293. Logger.Info("Stopping directory watching for path {0}", watcher.Path);
  294. watcher.EnableRaisingEvents = false;
  295. }
  296. }
  297. catch
  298. {
  299. }
  300. finally
  301. {
  302. RemoveWatcherFromList(watcher);
  303. }
  304. }
  305. /// <summary>
  306. /// Removes the watcher from list.
  307. /// </summary>
  308. /// <param name="watcher">The watcher.</param>
  309. private void RemoveWatcherFromList(FileSystemWatcher watcher)
  310. {
  311. FileSystemWatcher removed;
  312. _fileSystemWatchers.TryRemove(watcher.Path, out removed);
  313. }
  314. /// <summary>
  315. /// Handles the Error event of the watcher control.
  316. /// </summary>
  317. /// <param name="sender">The source of the event.</param>
  318. /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
  319. void watcher_Error(object sender, ErrorEventArgs e)
  320. {
  321. var ex = e.GetException();
  322. var dw = (FileSystemWatcher)sender;
  323. Logger.ErrorException("Error in Directory watcher for: " + dw.Path, ex);
  324. DisposeWatcher(dw);
  325. if (ConfigurationManager.Configuration.EnableLibraryMonitor == AutoOnOff.Auto)
  326. {
  327. Logger.Info("Disabling realtime monitor to prevent future instability");
  328. ConfigurationManager.Configuration.EnableLibraryMonitor = AutoOnOff.Disabled;
  329. Stop();
  330. }
  331. }
  332. /// <summary>
  333. /// Handles the Changed event of the watcher control.
  334. /// </summary>
  335. /// <param name="sender">The source of the event.</param>
  336. /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
  337. void watcher_Changed(object sender, FileSystemEventArgs e)
  338. {
  339. try
  340. {
  341. Logger.Debug("Changed detected of type " + e.ChangeType + " to " + e.FullPath);
  342. ReportFileSystemChanged(e.FullPath);
  343. }
  344. catch (Exception ex)
  345. {
  346. Logger.ErrorException("Exception in ReportFileSystemChanged. Path: {0}", ex, e.FullPath);
  347. }
  348. }
  349. public void ReportFileSystemChanged(string path)
  350. {
  351. if (string.IsNullOrEmpty(path))
  352. {
  353. throw new ArgumentNullException("path");
  354. }
  355. var filename = Path.GetFileName(path);
  356. var monitorPath = !(!string.IsNullOrEmpty(filename) && _alwaysIgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase));
  357. // Ignore certain files
  358. var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList();
  359. // If the parent of an ignored path has a change event, ignore that too
  360. if (tempIgnorePaths.Any(i =>
  361. {
  362. if (string.Equals(i, path, StringComparison.OrdinalIgnoreCase))
  363. {
  364. Logger.Debug("Ignoring change to {0}", path);
  365. return true;
  366. }
  367. if (_fileSystem.ContainsSubPath(i, path))
  368. {
  369. Logger.Debug("Ignoring change to {0}", path);
  370. return true;
  371. }
  372. // Go up a level
  373. var parent = Path.GetDirectoryName(i);
  374. if (!string.IsNullOrEmpty(parent))
  375. {
  376. if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase))
  377. {
  378. Logger.Debug("Ignoring change to {0}", path);
  379. return true;
  380. }
  381. }
  382. return false;
  383. }))
  384. {
  385. monitorPath = false;
  386. }
  387. if (monitorPath)
  388. {
  389. // Avoid implicitly captured closure
  390. CreateRefresher(path);
  391. }
  392. }
  393. private void CreateRefresher(string path)
  394. {
  395. var parentPath = Path.GetDirectoryName(path);
  396. lock (_activeRefreshers)
  397. {
  398. var refreshers = _activeRefreshers.ToList();
  399. foreach (var refresher in refreshers)
  400. {
  401. // Path is already being refreshed
  402. if (string.Equals(path, refresher.Path, StringComparison.Ordinal))
  403. {
  404. refresher.RestartTimer();
  405. return;
  406. }
  407. // Parent folder is already being refreshed
  408. if (_fileSystem.ContainsSubPath(refresher.Path, path))
  409. {
  410. refresher.AddPath(path);
  411. return;
  412. }
  413. // New path is a parent
  414. if (_fileSystem.ContainsSubPath(path, refresher.Path))
  415. {
  416. refresher.ResetPath(path, null);
  417. return;
  418. }
  419. // They are siblings. Rebase the refresher to the parent folder.
  420. if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal))
  421. {
  422. refresher.ResetPath(parentPath, path);
  423. return;
  424. }
  425. }
  426. var newRefresher = new FileRefresher(path, _fileSystem, ConfigurationManager, LibraryManager, TaskManager, Logger);
  427. newRefresher.Completed += NewRefresher_Completed;
  428. _activeRefreshers.Add(newRefresher);
  429. }
  430. }
  431. private void NewRefresher_Completed(object sender, EventArgs e)
  432. {
  433. var refresher = (FileRefresher)sender;
  434. DisposeRefresher(refresher);
  435. }
  436. /// <summary>
  437. /// Stops this instance.
  438. /// </summary>
  439. public void Stop()
  440. {
  441. LibraryManager.ItemAdded -= LibraryManager_ItemAdded;
  442. LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved;
  443. foreach (var watcher in _fileSystemWatchers.Values.ToList())
  444. {
  445. watcher.Created -= watcher_Changed;
  446. watcher.Deleted -= watcher_Changed;
  447. watcher.Renamed -= watcher_Changed;
  448. watcher.Changed -= watcher_Changed;
  449. try
  450. {
  451. watcher.EnableRaisingEvents = false;
  452. }
  453. catch (InvalidOperationException)
  454. {
  455. // Seeing this under mono on linux sometimes
  456. // Collection was modified; enumeration operation may not execute.
  457. }
  458. watcher.Dispose();
  459. }
  460. _fileSystemWatchers.Clear();
  461. DisposeRefreshers();
  462. }
  463. private void DisposeRefresher(FileRefresher refresher)
  464. {
  465. lock (_activeRefreshers)
  466. {
  467. refresher.Dispose();
  468. _activeRefreshers.Remove(refresher);
  469. }
  470. }
  471. private void DisposeRefreshers()
  472. {
  473. lock (_activeRefreshers)
  474. {
  475. foreach (var refresher in _activeRefreshers.ToList())
  476. {
  477. refresher.Dispose();
  478. }
  479. _activeRefreshers.Clear();
  480. }
  481. }
  482. /// <summary>
  483. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  484. /// </summary>
  485. public void Dispose()
  486. {
  487. Dispose(true);
  488. GC.SuppressFinalize(this);
  489. }
  490. /// <summary>
  491. /// Releases unmanaged and - optionally - managed resources.
  492. /// </summary>
  493. /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
  494. protected virtual void Dispose(bool dispose)
  495. {
  496. if (dispose)
  497. {
  498. Stop();
  499. }
  500. }
  501. }
  502. public class LibraryMonitorStartup : IServerEntryPoint
  503. {
  504. private readonly ILibraryMonitor _monitor;
  505. public LibraryMonitorStartup(ILibraryMonitor monitor)
  506. {
  507. _monitor = monitor;
  508. }
  509. public void Run()
  510. {
  511. _monitor.Start();
  512. }
  513. public void Dispose()
  514. {
  515. }
  516. }
  517. }