using System; using System.Collections.Concurrent; using System.IO; using JetBrains.Annotations; using WireMock.Validation; namespace WireMock.Util { /// /// An EnhancedFileSystemWatcher, which can be used to suppress duplicate events that fire on a single change to the file. /// /// public class EnhancedFileSystemWatcher : FileSystemWatcher { #region Private Members // Default Watch Interval in Milliseconds private const int DefaultWatchInterval = 100; // This Dictionary keeps the track of when an event occured last for a particular file private ConcurrentDictionary _lastFileEvent; // Watch Interval in Milliseconds private int _interval; // Timespan created when interval is set private TimeSpan _recentTimeSpan; #endregion #region Public Properties /// /// Interval, in milliseconds, within which events are considered "recent". /// [PublicAPI] public int Interval { get => _interval; set { _interval = value; // Set timespan based on the value passed _recentTimeSpan = new TimeSpan(0, 0, 0, 0, value); } } /// /// Allows user to set whether to filter recent events. /// If this is set a false, this class behaves like System.IO.FileSystemWatcher class. /// [PublicAPI] public bool FilterRecentEvents { get; set; } #endregion #region Constructors /// /// Initializes a new instance of the class. /// /// The interval. public EnhancedFileSystemWatcher(int interval = DefaultWatchInterval) { Check.Condition(interval, i => i >= 0, nameof(interval)); InitializeMembers(interval); } /// /// Initializes a new instance of the class. /// /// The directory to monitor, in standard or Universal Naming Convention (UNC) notation. /// The interval. public EnhancedFileSystemWatcher([NotNull] string path, int interval = DefaultWatchInterval) : base(path) { Check.NotNullOrEmpty(path, nameof(path)); Check.Condition(interval, i => i >= 0, nameof(interval)); InitializeMembers(interval); } /// /// Initializes a new instance of the class. /// /// The directory to monitor, in standard or Universal Naming Convention (UNC) notation. /// The type of files to watch. For example, "*.txt" watches for changes to all text files. /// The interval. public EnhancedFileSystemWatcher([NotNull] string path, [NotNull] string filter, int interval = DefaultWatchInterval) : base(path, filter) { Check.NotNullOrEmpty(path, nameof(path)); Check.NotNullOrEmpty(filter, nameof(filter)); Check.Condition(interval, i => i >= 0, nameof(interval)); InitializeMembers(interval); } #endregion #region Events // These events hide the events from the base class. // We want to raise these events appropriately and we do not want the // users of this class subscribing to these events of the base class accidentally /// /// Occurs when a file or directory in the specified is changed. /// public new event FileSystemEventHandler Changed; /// /// Occurs when a file or directory in the specified is created. /// public new event FileSystemEventHandler Created; /// /// Occurs when a file or directory in the specified is deleted. /// public new event FileSystemEventHandler Deleted; /// /// Occurs when a file or directory in the specified is renamed. /// public new event RenamedEventHandler Renamed; #endregion #region Protected Methods to raise the Events for this class /// /// Raises the event. /// /// A that contains the event data. protected new virtual void OnChanged(FileSystemEventArgs e) { Changed?.Invoke(this, e); } /// /// Raises the event. /// /// A that contains the event data. protected new virtual void OnCreated(FileSystemEventArgs e) { Created?.Invoke(this, e); } /// /// Raises the event. /// /// A that contains the event data. protected new virtual void OnDeleted(FileSystemEventArgs e) { Deleted?.Invoke(this, e); } /// /// Raises the event. /// /// A that contains the event data. protected new virtual void OnRenamed(RenamedEventArgs e) { Renamed?.Invoke(this, e); } #endregion #region Private Methods /// /// This Method Initializes the private members. /// Interval is set to its default value of 100 millisecond. /// FilterRecentEvents is set to true, _lastFileEvent dictionary is initialized. /// We subscribe to the base class events. /// private void InitializeMembers(int interval = 100) { Interval = interval; FilterRecentEvents = true; _lastFileEvent = new ConcurrentDictionary(); base.Created += OnCreated; base.Changed += OnChanged; base.Deleted += OnDeleted; base.Renamed += OnRenamed; } /// /// This method searches the dictionary to find out when the last event occured /// for a particular file. If that event occured within the specified timespan /// it returns true, else false /// /// The filename to be checked /// True if an event has occured within the specified interval, False otherwise private bool HasAnotherFileEventOccuredRecently(string fileName) { // Check dictionary only if user wants to filter recent events otherwise return value stays false. if (!FilterRecentEvents) { return false; } bool retVal = false; if (_lastFileEvent.ContainsKey(fileName)) { // If dictionary contains the filename, check how much time has elapsed // since the last event occured. If the timespan is less that the // specified interval, set return value to true // and store current datetime in dictionary for this file DateTime lastEventTime = _lastFileEvent[fileName]; DateTime currentTime = DateTime.Now; TimeSpan timeSinceLastEvent = currentTime - lastEventTime; retVal = timeSinceLastEvent < _recentTimeSpan; _lastFileEvent[fileName] = currentTime; } else { // If dictionary does not contain the filename, // no event has occured in past for this file, so set return value to false // and append filename along with current datetime to the dictionary _lastFileEvent.TryAdd(fileName, DateTime.Now); } return retVal; } #region FileSystemWatcher EventHandlers // Base class Event Handlers. Check if an event has occured recently and call method // to raise appropriate event only if no recent event is detected private void OnChanged(object sender, FileSystemEventArgs e) { if (!HasAnotherFileEventOccuredRecently(e.FullPath)) { OnChanged(e); } } private void OnCreated(object sender, FileSystemEventArgs e) { if (!HasAnotherFileEventOccuredRecently(e.FullPath)) { OnCreated(e); } } private void OnDeleted(object sender, FileSystemEventArgs e) { if (!HasAnotherFileEventOccuredRecently(e.FullPath)) { OnDeleted(e); } } private void OnRenamed(object sender, RenamedEventArgs e) { if (!HasAnotherFileEventOccuredRecently(e.OldFullPath)) { OnRenamed(e); } } #endregion #endregion } }