< Summary - Jellyfin

Information
Class: Emby.Server.Implementations.IO.LibraryMonitor
Assembly: Emby.Server.Implementations
File(s): /srv/git/jellyfin/Emby.Server.Implementations/IO/LibraryMonitor.cs
Line coverage
21%
Covered lines: 40
Uncovered lines: 147
Coverable lines: 187
Total lines: 479
Line coverage: 21.3%
Branch coverage
10%
Covered branches: 7
Total branches: 66
Branch coverage: 10.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

File(s)

/srv/git/jellyfin/Emby.Server.Implementations/IO/LibraryMonitor.cs

#LineLine coverage
 1using System;
 2using System.Collections.Concurrent;
 3using System.Collections.Generic;
 4using System.IO;
 5using System.Linq;
 6using System.Threading.Tasks;
 7using Emby.Server.Implementations.Library;
 8using MediaBrowser.Controller.Configuration;
 9using MediaBrowser.Controller.Entities;
 10using MediaBrowser.Controller.Library;
 11using MediaBrowser.Model.IO;
 12using Microsoft.Extensions.Hosting;
 13using Microsoft.Extensions.Logging;
 14
 15namespace Emby.Server.Implementations.IO
 16{
 17    /// <inheritdoc cref="ILibraryMonitor" />
 18    public sealed class LibraryMonitor : ILibraryMonitor, IDisposable
 19    {
 20        private readonly ILogger<LibraryMonitor> _logger;
 21        private readonly ILibraryManager _libraryManager;
 22        private readonly IServerConfigurationManager _configurationManager;
 23        private readonly IFileSystem _fileSystem;
 24
 25        /// <summary>
 26        /// The file system watchers.
 27        /// </summary>
 2228        private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new(StringComparer.Ordina
 29
 30        /// <summary>
 31        /// The affected paths.
 32        /// </summary>
 2233        private readonly List<FileRefresher> _activeRefreshers = [];
 34
 35        /// <summary>
 36        /// A dynamic list of paths that should be ignored.  Added to during our own file system modifications.
 37        /// </summary>
 2238        private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new(StringComparer.OrdinalIgnoreCase);
 39
 40        private bool _disposed;
 41
 42        /// <summary>
 43        /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.
 44        /// </summary>
 45        /// <param name="logger">The logger.</param>
 46        /// <param name="libraryManager">The library manager.</param>
 47        /// <param name="configurationManager">The configuration manager.</param>
 48        /// <param name="fileSystem">The filesystem.</param>
 49        /// <param name="appLifetime">The <see cref="IHostApplicationLifetime"/>.</param>
 50        public LibraryMonitor(
 51            ILogger<LibraryMonitor> logger,
 52            ILibraryManager libraryManager,
 53            IServerConfigurationManager configurationManager,
 54            IFileSystem fileSystem,
 55            IHostApplicationLifetime appLifetime)
 56        {
 2257            _libraryManager = libraryManager;
 2258            _logger = logger;
 2259            _configurationManager = configurationManager;
 2260            _fileSystem = fileSystem;
 61
 2262            appLifetime.ApplicationStarted.Register(Start);
 2263        }
 64
 65        /// <inheritdoc />
 66        public void ReportFileSystemChangeBeginning(string path)
 67        {
 068            ArgumentException.ThrowIfNullOrEmpty(path);
 69
 070            _tempIgnoredPaths[path] = path;
 071        }
 72
 73        /// <inheritdoc />
 74        public async void ReportFileSystemChangeComplete(string path, bool refreshPath)
 75        {
 76            ArgumentException.ThrowIfNullOrEmpty(path);
 77
 78            // This is an arbitrary amount of time, but delay it because file system writes often trigger events long af
 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, o
 81            await Task.Delay(45000).ConfigureAwait(false);
 82
 83            _tempIgnoredPaths.TryRemove(path, out _);
 84
 85            if (refreshPath)
 86            {
 87                try
 88                {
 89                    ReportFileSystemChanged(path);
 90                }
 91                catch (Exception ex)
 92                {
 93                    _logger.LogError(ex, "Error in ReportFileSystemChanged for {Path}", path);
 94                }
 95            }
 96        }
 97
 98        private bool IsLibraryMonitorEnabled(BaseItem item)
 99        {
 40100            if (item is BasePluginFolder)
 101            {
 40102                return false;
 103            }
 104
 0105            var options = _libraryManager.GetLibraryOptions(item);
 106
 0107            return options is not null && options.EnableRealtimeMonitor;
 108        }
 109
 110        /// <inheritdoc />
 111        public void Start()
 112        {
 43113            _libraryManager.ItemAdded += OnLibraryManagerItemAdded;
 43114            _libraryManager.ItemRemoved += OnLibraryManagerItemRemoved;
 115
 43116            var pathsToWatch = new List<string>();
 117
 43118            var paths = _libraryManager
 43119                .RootFolder
 43120                .Children
 43121                .Where(IsLibraryMonitorEnabled)
 43122                .OfType<Folder>()
 43123                .SelectMany(f => f.PhysicalLocations)
 43124                .Distinct(StringComparer.OrdinalIgnoreCase)
 43125                .Order();
 126
 86127            foreach (var path in paths)
 128            {
 0129                if (!ContainsParentFolder(pathsToWatch, path))
 130                {
 0131                    pathsToWatch.Add(path);
 132                }
 133            }
 134
 86135            foreach (var path in pathsToWatch)
 136            {
 0137                StartWatchingPath(path);
 138            }
 43139        }
 140
 141        private void StartWatching(BaseItem item)
 142        {
 0143            if (IsLibraryMonitorEnabled(item))
 144            {
 0145                StartWatchingPath(item.Path);
 146            }
 0147        }
 148
 149        /// <summary>
 150        /// Handles the ItemRemoved event of the LibraryManager control.
 151        /// </summary>
 152        /// <param name="sender">The source of the event.</param>
 153        /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
 154        private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
 155        {
 0156            if (e.Parent is AggregateFolder)
 157            {
 0158                StopWatchingPath(e.Item.Path);
 159            }
 0160        }
 161
 162        /// <summary>
 163        /// Handles the ItemAdded event of the LibraryManager control.
 164        /// </summary>
 165        /// <param name="sender">The source of the event.</param>
 166        /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
 167        private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
 168        {
 0169            if (e.Parent is AggregateFolder)
 170            {
 0171                StartWatching(e.Item);
 172            }
 0173        }
 174
 175        /// <summary>
 176        /// Examine a list of strings assumed to be file paths to see if it contains a parent of
 177        /// the provided path.
 178        /// </summary>
 179        /// <param name="lst">The LST.</param>
 180        /// <param name="path">The path.</param>
 181        /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
 182        /// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
 183        private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path)
 184        {
 0185            if (path.IsEmpty)
 186            {
 0187                throw new ArgumentException("Path can't be empty", nameof(path));
 188            }
 189
 0190            path = path.TrimEnd(Path.DirectorySeparatorChar);
 191
 0192            foreach (var str in lst)
 193            {
 194                // this should be a little quicker than examining each actual parent folder...
 0195                var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
 196
 0197                if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
 0198                    || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.Dir
 199                {
 0200                    return true;
 201                }
 202            }
 203
 0204            return false;
 0205        }
 206
 207        /// <summary>
 208        /// Starts the watching path.
 209        /// </summary>
 210        /// <param name="path">The path.</param>
 211        private void StartWatchingPath(string path)
 212        {
 0213            if (!Directory.Exists(path))
 214            {
 215                // Seeing a crash in the mono runtime due to an exception being thrown on a different thread
 0216                _logger.LogInformation("Skipping realtime monitor for {Path} because the path does not exist", path);
 0217                return;
 218            }
 219
 220            // Already being watched
 0221            if (_fileSystemWatchers.ContainsKey(path))
 222            {
 0223                return;
 224            }
 225
 226            // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do t
 0227            Task.Run(() =>
 0228            {
 0229                try
 0230                {
 0231                    var newWatcher = new FileSystemWatcher(path, "*")
 0232                    {
 0233                        IncludeSubdirectories = true,
 0234                        InternalBufferSize = 65536,
 0235                        NotifyFilter = NotifyFilters.CreationTime |
 0236                                       NotifyFilters.DirectoryName |
 0237                                       NotifyFilters.FileName |
 0238                                       NotifyFilters.LastWrite |
 0239                                       NotifyFilters.Size |
 0240                                       NotifyFilters.Attributes
 0241                    };
 0242
 0243                    newWatcher.Created += OnWatcherChanged;
 0244                    newWatcher.Deleted += OnWatcherChanged;
 0245                    newWatcher.Renamed += OnWatcherChanged;
 0246                    newWatcher.Changed += OnWatcherChanged;
 0247                    newWatcher.Error += OnWatcherError;
 0248
 0249                    if (_fileSystemWatchers.TryAdd(path, newWatcher))
 0250                    {
 0251                        newWatcher.EnableRaisingEvents = true;
 0252                        _logger.LogInformation("Watching directory {Path}", path);
 0253                    }
 0254                    else
 0255                    {
 0256                        DisposeWatcher(newWatcher, false);
 0257                    }
 0258                }
 0259                catch (Exception ex)
 0260                {
 0261                    _logger.LogError(ex, "Error watching path: {Path}", path);
 0262                }
 0263            });
 0264        }
 265
 266        /// <summary>
 267        /// Stops the watching path.
 268        /// </summary>
 269        /// <param name="path">The path.</param>
 270        private void StopWatchingPath(string path)
 271        {
 0272            if (_fileSystemWatchers.TryGetValue(path, out var watcher))
 273            {
 0274                DisposeWatcher(watcher, true);
 275            }
 0276        }
 277
 278        /// <summary>
 279        /// Disposes the watcher.
 280        /// </summary>
 281        private void DisposeWatcher(FileSystemWatcher watcher, bool removeFromList)
 282        {
 283            try
 284            {
 0285                using (watcher)
 286                {
 0287                    _logger.LogInformation("Stopping directory watching for path {Path}", watcher.Path);
 288
 0289                    watcher.Created -= OnWatcherChanged;
 0290                    watcher.Deleted -= OnWatcherChanged;
 0291                    watcher.Renamed -= OnWatcherChanged;
 0292                    watcher.Changed -= OnWatcherChanged;
 0293                    watcher.Error -= OnWatcherError;
 294
 0295                    watcher.EnableRaisingEvents = false;
 0296                }
 297            }
 298            finally
 299            {
 0300                if (removeFromList)
 301                {
 0302                    _fileSystemWatchers.TryRemove(watcher.Path, out _);
 303                }
 0304            }
 0305        }
 306
 307        /// <summary>
 308        /// Handles the Error event of the watcher control.
 309        /// </summary>
 310        /// <param name="sender">The source of the event.</param>
 311        /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>
 312        private void OnWatcherError(object sender, ErrorEventArgs e)
 313        {
 0314            var ex = e.GetException();
 0315            var dw = (FileSystemWatcher)sender;
 316
 0317            _logger.LogError(ex, "Error in Directory watcher for: {Path}", dw.Path);
 318
 0319            DisposeWatcher(dw, true);
 0320        }
 321
 322        /// <summary>
 323        /// Handles the Changed event of the watcher control.
 324        /// </summary>
 325        /// <param name="sender">The source of the event.</param>
 326        /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>
 327        private void OnWatcherChanged(object sender, FileSystemEventArgs e)
 328        {
 329            try
 330            {
 0331                ReportFileSystemChanged(e.FullPath);
 0332            }
 0333            catch (Exception ex)
 334            {
 0335                _logger.LogError(ex, "Exception in ReportFileSystemChanged. Path: {FullPath}", e.FullPath);
 0336            }
 0337        }
 338
 339        /// <inheritdoc />
 340        public void ReportFileSystemChanged(string path)
 341        {
 0342            ArgumentException.ThrowIfNullOrEmpty(path);
 343
 0344            if (IgnorePatterns.ShouldIgnore(path))
 345            {
 0346                return;
 347            }
 348
 349            // Ignore certain files, If the parent of an ignored path has a change event, ignore that too
 0350            foreach (var i in _tempIgnoredPaths.Keys)
 351            {
 0352                if (_fileSystem.AreEqual(i, path)
 0353                    || _fileSystem.ContainsSubPath(i, path))
 354                {
 0355                    _logger.LogDebug("Ignoring change to {Path}", path);
 0356                    return;
 357                }
 358
 359                // Go up a level
 0360                var parent = Path.GetDirectoryName(i);
 0361                if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
 362                {
 0363                    _logger.LogDebug("Ignoring change to {Path}", path);
 0364                    return;
 365                }
 366            }
 367
 0368            CreateRefresher(path);
 0369        }
 370
 371        private void CreateRefresher(string path)
 372        {
 0373            var parentPath = Path.GetDirectoryName(path);
 374
 0375            lock (_activeRefreshers)
 376            {
 0377                foreach (var refresher in _activeRefreshers)
 378                {
 379                    // Path is already being refreshed
 0380                    if (_fileSystem.AreEqual(path, refresher.Path))
 381                    {
 0382                        refresher.RestartTimer();
 0383                        return;
 384                    }
 385
 386                    // Parent folder is already being refreshed
 0387                    if (_fileSystem.ContainsSubPath(refresher.Path, path))
 388                    {
 0389                        refresher.AddPath(path);
 0390                        return;
 391                    }
 392
 393                    // New path is a parent
 0394                    if (_fileSystem.ContainsSubPath(path, refresher.Path))
 395                    {
 0396                        refresher.ResetPath(path, null);
 0397                        return;
 398                    }
 399
 400                    // They are siblings. Rebase the refresher to the parent folder.
 0401                    if (parentPath is not null
 0402                        && Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
 403                    {
 0404                        refresher.ResetPath(parentPath, path);
 0405                        return;
 406                    }
 407                }
 408
 0409                var newRefresher = new FileRefresher(path, _configurationManager, _libraryManager, _logger);
 0410                newRefresher.Completed += OnNewRefresherCompleted;
 0411                _activeRefreshers.Add(newRefresher);
 0412            }
 0413        }
 414
 415        private void OnNewRefresherCompleted(object? sender, EventArgs e)
 416        {
 0417            if (sender is null)
 418            {
 0419                return;
 420            }
 421
 0422            var refresher = (FileRefresher)sender;
 0423            DisposeRefresher(refresher);
 0424        }
 425
 426        /// <summary>
 427        /// Stops this instance.
 428        /// </summary>
 429        public void Stop()
 430        {
 45431            _libraryManager.ItemAdded -= OnLibraryManagerItemAdded;
 45432            _libraryManager.ItemRemoved -= OnLibraryManagerItemRemoved;
 433
 90434            foreach (var watcher in _fileSystemWatchers.Values.ToList())
 435            {
 0436                DisposeWatcher(watcher, false);
 437            }
 438
 45439            _fileSystemWatchers.Clear();
 45440            DisposeRefreshers();
 45441        }
 442
 443        private void DisposeRefresher(FileRefresher refresher)
 444        {
 0445            lock (_activeRefreshers)
 446            {
 0447                refresher.Completed -= OnNewRefresherCompleted;
 0448                refresher.Dispose();
 0449                _activeRefreshers.Remove(refresher);
 0450            }
 0451        }
 452
 453        private void DisposeRefreshers()
 454        {
 45455            lock (_activeRefreshers)
 456            {
 90457                foreach (var refresher in _activeRefreshers)
 458                {
 0459                    refresher.Completed -= OnNewRefresherCompleted;
 0460                    refresher.Dispose();
 461                }
 462
 45463                _activeRefreshers.Clear();
 45464            }
 45465        }
 466
 467        /// <inheritdoc />
 468        public void Dispose()
 469        {
 22470            if (_disposed)
 471            {
 0472                return;
 473            }
 474
 22475            Stop();
 22476            _disposed = true;
 22477        }
 478    }
 479}