| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727 | using MediaBrowser.Common.IO;using MediaBrowser.Common.ScheduledTasks;using MediaBrowser.Controller.Configuration;using MediaBrowser.Controller.Entities;using MediaBrowser.Controller.Library;using MediaBrowser.Controller.Plugins;using MediaBrowser.Model.Configuration;using MediaBrowser.Model.Logging;using MediaBrowser.Server.Implementations.ScheduledTasks;using Microsoft.Win32;using System;using System.Collections.Concurrent;using System.Collections.Generic;using System.IO;using System.Linq;using System.Threading;using System.Threading.Tasks;using CommonIO;using MediaBrowser.Controller;namespace MediaBrowser.Server.Implementations.IO{    public class LibraryMonitor : ILibraryMonitor    {        /// <summary>        /// The file system watchers        /// </summary>        private readonly ConcurrentDictionary<string, FileSystemWatcher> _fileSystemWatchers = new ConcurrentDictionary<string, FileSystemWatcher>(StringComparer.OrdinalIgnoreCase);        /// <summary>        /// The update timer        /// </summary>        private Timer _updateTimer;        /// <summary>        /// The affected paths        /// </summary>        private readonly ConcurrentDictionary<string, string> _affectedPaths = new ConcurrentDictionary<string, string>();        /// <summary>        /// A dynamic list of paths that should be ignored.  Added to during our own file sytem modifications.        /// </summary>        private readonly ConcurrentDictionary<string, string> _tempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);        /// <summary>        /// Any file name ending in any of these will be ignored by the watchers        /// </summary>        private readonly IReadOnlyList<string> _alwaysIgnoreFiles = new List<string>        {            "thumbs.db",             "small.jpg",             "albumart.jpg",            // WMC temp recording directories that will constantly be written to            "TempRec",            "TempSBE"        };        /// <summary>        /// The timer lock        /// </summary>        private readonly object _timerLock = new object();        /// <summary>        /// Add the path to our temporary ignore list.  Use when writing to a path within our listening scope.        /// </summary>        /// <param name="path">The path.</param>        private void TemporarilyIgnore(string path)        {            _tempIgnoredPaths[path] = path;        }        public void ReportFileSystemChangeBeginning(string path)        {            if (string.IsNullOrEmpty(path))            {                throw new ArgumentNullException("path");            }            TemporarilyIgnore(path);        }        public async void ReportFileSystemChangeComplete(string path, bool refreshPath)        {            if (string.IsNullOrEmpty(path))            {                throw new ArgumentNullException("path");            }            // 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.            // Seeing long delays in some situations, especially over the network, sometimes up to 45 seconds            // But if we make this delay too high, we risk missing legitimate changes, such as user adding a new file, or hand-editing metadata            await Task.Delay(20000).ConfigureAwait(false);            string val;            _tempIgnoredPaths.TryRemove(path, out val);            if (refreshPath)            {                ReportFileSystemChanged(path);            }        }        /// <summary>        /// Gets or sets the logger.        /// </summary>        /// <value>The logger.</value>        private ILogger Logger { get; set; }        /// <summary>        /// Gets or sets the task manager.        /// </summary>        /// <value>The task manager.</value>        private ITaskManager TaskManager { get; set; }        private ILibraryManager LibraryManager { get; set; }        private IServerConfigurationManager ConfigurationManager { get; set; }        private readonly IFileSystem _fileSystem;        private readonly IServerApplicationHost _appHost;        /// <summary>        /// Initializes a new instance of the <see cref="LibraryMonitor" /> class.        /// </summary>        public LibraryMonitor(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem, IServerApplicationHost appHost)        {            if (taskManager == null)            {                throw new ArgumentNullException("taskManager");            }            LibraryManager = libraryManager;            TaskManager = taskManager;            Logger = logManager.GetLogger(GetType().Name);            ConfigurationManager = configurationManager;            _fileSystem = fileSystem;            _appHost = appHost;            SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged;        }        /// <summary>        /// Handles the PowerModeChanged event of the SystemEvents control.        /// </summary>        /// <param name="sender">The source of the event.</param>        /// <param name="e">The <see cref="PowerModeChangedEventArgs"/> instance containing the event data.</param>        void SystemEvents_PowerModeChanged(object sender, PowerModeChangedEventArgs e)        {            Restart();        }        private void Restart()        {            Stop();            Start();        }        private bool EnableLibraryMonitor        {            get            {                switch (ConfigurationManager.Configuration.EnableLibraryMonitor)                {                    case AutoOnOff.Auto:                        return _appHost.SupportsLibraryMonitor;                    case AutoOnOff.Enabled:                        return true;                    default:                        return false;                }            }        }        public void Start()        {            if (EnableLibraryMonitor)            {                StartInternal();            }        }        /// <summary>        /// Starts this instance.        /// </summary>        private void StartInternal()        {            LibraryManager.ItemAdded += LibraryManager_ItemAdded;            LibraryManager.ItemRemoved += LibraryManager_ItemRemoved;            var pathsToWatch = new List<string> { LibraryManager.RootFolder.Path };            var paths = LibraryManager                .RootFolder                .Children                .OfType<Folder>()                .SelectMany(f => f.PhysicalLocations)                .Distinct(StringComparer.OrdinalIgnoreCase)                .OrderBy(i => i)                .ToList();            foreach (var path in paths)            {                if (!ContainsParentFolder(pathsToWatch, path))                {                    pathsToWatch.Add(path);                }            }            foreach (var path in pathsToWatch)            {                StartWatchingPath(path);            }        }        /// <summary>        /// Handles the ItemRemoved event of the LibraryManager control.        /// </summary>        /// <param name="sender">The source of the event.</param>        /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>        void LibraryManager_ItemRemoved(object sender, ItemChangeEventArgs e)        {            if (e.Item.Parent is AggregateFolder)            {                StopWatchingPath(e.Item.Path);            }        }        /// <summary>        /// Handles the ItemAdded event of the LibraryManager control.        /// </summary>        /// <param name="sender">The source of the event.</param>        /// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>        void LibraryManager_ItemAdded(object sender, ItemChangeEventArgs e)        {            if (e.Item.Parent is AggregateFolder)            {                StartWatchingPath(e.Item.Path);            }        }        /// <summary>        /// Examine a list of strings assumed to be file paths to see if it contains a parent of        /// the provided path.        /// </summary>        /// <param name="lst">The LST.</param>        /// <param name="path">The path.</param>        /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>        /// <exception cref="System.ArgumentNullException">path</exception>        private static bool ContainsParentFolder(IEnumerable<string> lst, string path)        {            if (string.IsNullOrEmpty(path))            {                throw new ArgumentNullException("path");            }            path = path.TrimEnd(Path.DirectorySeparatorChar);            return lst.Any(str =>            {                //this should be a little quicker than examining each actual parent folder...                var compare = str.TrimEnd(Path.DirectorySeparatorChar);                return (path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar));            });        }        /// <summary>        /// Starts the watching path.        /// </summary>        /// <param name="path">The path.</param>        private void StartWatchingPath(string path)        {            // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel            Task.Run(() =>            {                try                {                    var newWatcher = new FileSystemWatcher(path, "*")                    {                        IncludeSubdirectories = true,                        InternalBufferSize = 32767                    };                    newWatcher.NotifyFilter = NotifyFilters.CreationTime |                        NotifyFilters.DirectoryName |                        NotifyFilters.FileName |                        NotifyFilters.LastWrite |                        NotifyFilters.Size |                        NotifyFilters.Attributes;                    newWatcher.Created += watcher_Changed;                    newWatcher.Deleted += watcher_Changed;                    newWatcher.Renamed += watcher_Changed;                    newWatcher.Changed += watcher_Changed;                    newWatcher.Error += watcher_Error;                    if (_fileSystemWatchers.TryAdd(path, newWatcher))                    {                        newWatcher.EnableRaisingEvents = true;                        Logger.Info("Watching directory " + path);                    }                    else                    {                        Logger.Info("Unable to add directory watcher for {0}. It already exists in the dictionary.", path);                        newWatcher.Dispose();                    }                }                catch (Exception ex)                {                    Logger.ErrorException("Error watching path: {0}", ex, path);                }            });        }        /// <summary>        /// Stops the watching path.        /// </summary>        /// <param name="path">The path.</param>        private void StopWatchingPath(string path)        {            FileSystemWatcher watcher;            if (_fileSystemWatchers.TryGetValue(path, out watcher))            {                DisposeWatcher(watcher);            }        }        /// <summary>        /// Disposes the watcher.        /// </summary>        /// <param name="watcher">The watcher.</param>        private void DisposeWatcher(FileSystemWatcher watcher)        {            try            {                using (watcher)                {                    Logger.Info("Stopping directory watching for path {0}", watcher.Path);                    watcher.EnableRaisingEvents = false;                }            }            catch            {            }            finally            {                RemoveWatcherFromList(watcher);            }        }        /// <summary>        /// Removes the watcher from list.        /// </summary>        /// <param name="watcher">The watcher.</param>        private void RemoveWatcherFromList(FileSystemWatcher watcher)        {            FileSystemWatcher removed;            _fileSystemWatchers.TryRemove(watcher.Path, out removed);        }        /// <summary>        /// Handles the Error event of the watcher control.        /// </summary>        /// <param name="sender">The source of the event.</param>        /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param>        void watcher_Error(object sender, ErrorEventArgs e)        {            var ex = e.GetException();            var dw = (FileSystemWatcher)sender;            Logger.ErrorException("Error in Directory watcher for: " + dw.Path, ex);            DisposeWatcher(dw);            if (ConfigurationManager.Configuration.EnableLibraryMonitor == AutoOnOff.Auto)            {                Logger.Info("Disabling realtime monitor to prevent future instability");                ConfigurationManager.Configuration.EnableLibraryMonitor = AutoOnOff.Disabled;                Stop();            }        }        /// <summary>        /// Handles the Changed event of the watcher control.        /// </summary>        /// <param name="sender">The source of the event.</param>        /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param>        void watcher_Changed(object sender, FileSystemEventArgs e)        {            try            {                Logger.Debug("Changed detected of type " + e.ChangeType + " to " + e.FullPath);                ReportFileSystemChanged(e.FullPath);            }            catch (Exception ex)            {                Logger.ErrorException("Exception in ReportFileSystemChanged. Path: {0}", ex, e.FullPath);            }        }        public void ReportFileSystemChanged(string path)        {            if (string.IsNullOrEmpty(path))            {                throw new ArgumentNullException("path");            }            var filename = Path.GetFileName(path);            var monitorPath = !(!string.IsNullOrEmpty(filename) && _alwaysIgnoreFiles.Contains(filename, StringComparer.OrdinalIgnoreCase));            // Ignore certain files            var tempIgnorePaths = _tempIgnoredPaths.Keys.ToList();            // If the parent of an ignored path has a change event, ignore that too            if (tempIgnorePaths.Any(i =>            {                if (string.Equals(i, path, StringComparison.OrdinalIgnoreCase))                {                    Logger.Debug("Ignoring change to {0}", path);                    return true;                }                if (_fileSystem.ContainsSubPath(i, path))                {                    Logger.Debug("Ignoring change to {0}", path);                    return true;                }                // Go up a level                var parent = Path.GetDirectoryName(i);                if (!string.IsNullOrEmpty(parent))                {                    if (string.Equals(parent, path, StringComparison.OrdinalIgnoreCase))                    {                        Logger.Debug("Ignoring change to {0}", path);                        return true;                    }                }                return false;            }))            {                monitorPath = false;            }            if (monitorPath)            {                // Avoid implicitly captured closure                var affectedPath = path;                _affectedPaths.AddOrUpdate(path, path, (key, oldValue) => affectedPath);            }            RestartTimer();        }        private void RestartTimer()        {            lock (_timerLock)            {                if (_updateTimer == null)                {                    _updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(ConfigurationManager.Configuration.RealtimeLibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));                }                else                {                    _updateTimer.Change(TimeSpan.FromSeconds(ConfigurationManager.Configuration.RealtimeLibraryMonitorDelay), TimeSpan.FromMilliseconds(-1));                }            }        }        /// <summary>        /// Timers the stopped.        /// </summary>        /// <param name="stateInfo">The state info.</param>        private async void TimerStopped(object stateInfo)        {            // Extend the timer as long as any of the paths are still being written to.            if (_affectedPaths.Any(p => IsFileLocked(p.Key)))            {                Logger.Info("Timer extended.");                RestartTimer();                return;            }            Logger.Debug("Timer stopped.");            DisposeTimer();            var paths = _affectedPaths.Keys.ToList();            _affectedPaths.Clear();            try            {                await ProcessPathChanges(paths).ConfigureAwait(false);            }            catch (Exception ex)            {                Logger.ErrorException("Error processing directory changes", ex);            }        }        private bool IsFileLocked(string path)        {            try            {                var data = _fileSystem.GetFileSystemInfo(path);                if (!data.Exists                    || data.Attributes.HasFlag(FileAttributes.Directory)                    // Opening a writable stream will fail with readonly files                    || data.Attributes.HasFlag(FileAttributes.ReadOnly))                {                    return false;                }            }            catch (IOException)            {                return false;            }            catch (Exception ex)            {                Logger.ErrorException("Error getting file system info for: {0}", ex, path);                return false;            }            try            {                using (_fileSystem.GetFileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))                {                    if (_updateTimer != null)                    {                        //file is not locked                        return false;                    }                }            }            catch (DirectoryNotFoundException)            {                // File may have been deleted                return false;            }            catch (FileNotFoundException)            {                // File may have been deleted                return false;            }            catch (IOException)            {                //the file is unavailable because it is:                //still being written to                //or being processed by another thread                //or does not exist (has already been processed)                Logger.Debug("{0} is locked.", path);                return true;            }            catch (Exception ex)            {                Logger.ErrorException("Error determining if file is locked: {0}", ex, path);                return false;            }            return false;        }        private void DisposeTimer()        {            lock (_timerLock)            {                if (_updateTimer != null)                {                    _updateTimer.Dispose();                    _updateTimer = null;                }            }        }        /// <summary>        /// Processes the path changes.        /// </summary>        /// <param name="paths">The paths.</param>        /// <returns>Task.</returns>        private async Task ProcessPathChanges(List<string> paths)        {            var itemsToRefresh = paths                .Select(GetAffectedBaseItem)                .Where(item => item != null)                .Distinct()                .ToList();            foreach (var p in paths)            {                Logger.Info(p + " reports change.");            }            // If the root folder changed, run the library task so the user can see it            if (itemsToRefresh.Any(i => i is AggregateFolder))            {                TaskManager.CancelIfRunningAndQueue<RefreshMediaLibraryTask>();                return;            }            foreach (var item in itemsToRefresh)            {                Logger.Info(item.Name + " (" + item.Path + ") will be refreshed.");                try                {                    await item.ChangedExternally().ConfigureAwait(false);                }                catch (IOException ex)                {                    // For now swallow and log.                     // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)                    // Should we remove it from it's parent?                    Logger.ErrorException("Error refreshing {0}", ex, item.Name);                }                catch (Exception ex)                {                    Logger.ErrorException("Error refreshing {0}", ex, item.Name);                }            }        }        /// <summary>        /// Gets the affected base item.        /// </summary>        /// <param name="path">The path.</param>        /// <returns>BaseItem.</returns>        private BaseItem GetAffectedBaseItem(string path)        {            BaseItem item = null;            while (item == null && !string.IsNullOrEmpty(path))            {                item = LibraryManager.RootFolder.FindByPath(path);                path = Path.GetDirectoryName(path);            }            if (item != null)            {                // If the item has been deleted find the first valid parent that still exists				while (!_fileSystem.DirectoryExists(item.Path) && !_fileSystem.FileExists(item.Path))                {                    item = item.Parent;                    if (item == null)                    {                        break;                    }                }            }            return item;        }        /// <summary>        /// Stops this instance.        /// </summary>        public void Stop()        {            LibraryManager.ItemAdded -= LibraryManager_ItemAdded;            LibraryManager.ItemRemoved -= LibraryManager_ItemRemoved;            foreach (var watcher in _fileSystemWatchers.Values.ToList())            {                watcher.Changed -= watcher_Changed;                watcher.EnableRaisingEvents = false;                watcher.Dispose();            }            DisposeTimer();            _fileSystemWatchers.Clear();            _affectedPaths.Clear();        }        /// <summary>        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.        /// </summary>        public void Dispose()        {            Dispose(true);            GC.SuppressFinalize(this);        }        /// <summary>        /// Releases unmanaged and - optionally - managed resources.        /// </summary>        /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>        protected virtual void Dispose(bool dispose)        {            if (dispose)            {                Stop();            }        }    }    public class LibraryMonitorStartup : IServerEntryPoint    {        private readonly ILibraryMonitor _monitor;        public LibraryMonitorStartup(ILibraryMonitor monitor)        {            _monitor = monitor;        }        public void Run()        {            _monitor.Start();        }        public void Dispose()        {        }    }}
 |