//****************************************************************************************************** // FileTracker.cs - Gbtc // // Copyright © 2014, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 04/29/2014 - Stephen C. Wills // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Timers; using GSF.Collections; using GSF.Threading; using CancellationToken = GSF.Threading.CancellationToken; using Timer = System.Timers.Timer; namespace GSF.IO { /// /// Defines strategies for enumerating /// files in the watch directories. /// public enum FileEnumerationStrategy { /// /// Enumerate all files sequentially. /// Sequential, /// /// Enumerates the watch directories in parallel, /// but subdirectories are processed sequentially. /// ParallelWatchDirectories, /// /// Enumerates every directory, including subdirectories, in parallel. /// ParallelSubdirectories, /// /// Does not enumerate directories, relying only on /// the file watcher to handle file processing events. /// None } /// /// Arguments to events triggered by the . /// public class FileProcessorEventArgs : EventArgs { #region [ Members ] // Fields private readonly string m_fullPath; private readonly bool m_alreadyProcessed; private readonly bool m_raisedByFileWatcher; private readonly Func m_retryCounter; private bool m_requeue; #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// /// The full path to the file to be processed. /// Flag indicating whether this file has been processed before. /// Flag indicating whether this event was raised by the file watcher. /// The function that provides the value for . public FileProcessorEventArgs(string fullPath, bool alreadyProcessed, bool raisedByFileWatcher, Func retryCounter) { m_fullPath = fullPath; m_alreadyProcessed = alreadyProcessed; m_raisedByFileWatcher = raisedByFileWatcher; m_retryCounter = retryCounter; } #endregion #region [ Properties ] /// /// Gets the full path to the file to be processed. /// public string FullPath { get { return m_fullPath; } } /// /// Gets the flag that indicates whether this file has been processed before. /// public bool AlreadyProcessed { get { return m_alreadyProcessed; } } /// /// Gets the flag that indicates whether this event was raised by the file watcher. /// public bool RaisedByFileWatcher { get { return m_raisedByFileWatcher; } } /// /// Gets the number of file processing attempts. /// public int RetryCount { get { return m_retryCounter(); } } /// /// Gets or sets a flag that indicates whether the /// file should be requeued by the file processor. /// public bool Requeue { get { return m_requeue; } set { m_requeue = value; } } #endregion } /// /// Tracks files processed in a list of directories, and /// notifies when new files become available to be processed. /// public sealed class FileProcessor : IDisposable { #region [ Members ] // Nested Types private sealed class TrackedDirectory : IDisposable { #region [ Members ] // Fields private FileProcessor m_fileProcessor; private SafeFileWatcher m_fileWatcher; private FileEnumerator m_fileEnumerator; private bool m_disposed; #endregion #region [ Constructors ] public TrackedDirectory(FileProcessor fileProcessor, string path) { m_fileProcessor = fileProcessor; Path = path; CreateFileWatcher(); } #endregion #region [ Properties ] public string Path { get; } public bool IsEnumerating { get { return m_fileEnumerator?.Active ?? false; } } public List ActivelyEnumeratedPaths { get { return m_fileEnumerator.ActivelyVisitedPaths; } } #endregion #region [ Methods ] public void EnumerateFiles(FileEnumerationStrategy fileEnumerationStrategy) { if (m_disposed) return; if (IsEnumerating) throw new InvalidOperationException("Enumeration is already in progress."); CancellationToken cancellationToken = new CancellationToken(); m_fileEnumerator = new FileEnumerator(Path, m_fileProcessor, cancellationToken); m_fileEnumerator.Enumerate(fileEnumerationStrategy); } public void CancelEnumeration() { m_fileEnumerator?.Cancel(); } public void CheckFileWatcher() { if (m_disposed) return; if (!m_fileWatcher.EnableRaisingEvents) { DisposeFileWatcher(); CreateFileWatcher(); } } /// /// Releases all the resources used by the object. /// public void Dispose() { if (m_disposed) return; try { CancelEnumeration(); DisposeFileWatcher(); } finally { m_disposed = true; } } private void CreateFileWatcher() { m_fileWatcher = new SafeFileWatcher(Path); m_fileWatcher.InternalBufferSize = m_fileProcessor.InternalBufferSize; m_fileWatcher.IncludeSubdirectories = true; m_fileWatcher.Created += m_fileProcessor.Watcher_Created; m_fileWatcher.Changed += m_fileProcessor.Watcher_Changed; m_fileWatcher.Renamed += m_fileProcessor.Watcher_Renamed; m_fileWatcher.Deleted += m_fileProcessor.Watcher_Deleted; m_fileWatcher.Error += m_fileProcessor.Watcher_Error; m_fileWatcher.EnableRaisingEvents = true; } private void DisposeFileWatcher() { m_fileWatcher.Created -= m_fileProcessor.Watcher_Created; m_fileWatcher.Changed -= m_fileProcessor.Watcher_Changed; m_fileWatcher.Renamed -= m_fileProcessor.Watcher_Renamed; m_fileWatcher.Deleted -= m_fileProcessor.Watcher_Deleted; m_fileWatcher.Error -= m_fileProcessor.Watcher_Error; m_fileWatcher.Dispose(); } #endregion } private class FileEnumerator { #region [ Members ] // Fields private readonly FileProcessor m_fileProcessor; private readonly CancellationToken m_cancellationToken; private string m_lastVisitedPath; private bool m_active; #endregion #region [ Constructors ] public FileEnumerator(string path, FileProcessor fileProcessor, CancellationToken cancellationToken) { Path = path; m_fileProcessor = fileProcessor; m_cancellationToken = cancellationToken; } #endregion #region [ Properties ] public string Path { get; } public FileEnumerator[] SubdirectoryEnumerators { get; private set; } = new FileEnumerator[0]; public bool Active { get { if (m_active) return true; FileEnumerator[] subdirectoryEnumerators = SubdirectoryEnumerators; return m_active || subdirectoryEnumerators.Any(enumerator => enumerator.Active); } } public List ActivelyVisitedPaths { get { List activelyVisitedPaths = new List(); BuildActivelyVisitedPaths(activelyVisitedPaths); return activelyVisitedPaths; } } #endregion #region [ Methods ] public void Enumerate(FileEnumerationStrategy enumerationStrategy) { if (Active) throw new InvalidOperationException("Enumeration is already in progress."); if ((object)LogicalThread.CurrentThread == null) throw new InvalidOperationException("Enumeration must be executed on a logical thread."); m_active = true; if (enumerationStrategy != FileEnumerationStrategy.ParallelSubdirectories) { SubdirectoryEnumerators = new FileEnumerator[0]; // If subdirectories do not need to be processed in parallel, // we can simply process a recursive call to enumerate all files IEnumerable fileEnumerable = FilePath.EnumerateFiles(Path, exceptionHandler: m_fileProcessor.OnError); EnumerateNextFile(new EnumerableWrapper(fileEnumerable, m_cancellationToken)); } else { // To process subdirectories in parallel, we need to create a recursive structure // of enumerators to process each subdirectory on its own logical thread InitializeSubdirectoryEnumerators(); EnumerateNextDirectory(0); } } public void Cancel() { m_cancellationToken.Cancel(); } // Initializes the FileEnumerators for subdirectories. private void InitializeSubdirectoryEnumerators() { // Immediately enumerate all subdirectories so we can build the array of FileEnumerators. // Using a fixed-size collection like an array greatly simplifies multithreaded access to the collection. using (EnumerableWrapper wrapper = new EnumerableWrapper(EnumerateDirectories(), m_cancellationToken)) { List subDirectories = new List(); while (wrapper.MoveNext()) subDirectories.Add(wrapper.Current); SubdirectoryEnumerators = subDirectories .Select(path => new FileEnumerator(path, m_fileProcessor, m_cancellationToken)) .ToArray(); } } // Enumerates the next subdirectory in the list of subdirectory enumerators. private void EnumerateNextDirectory(int index) { try { m_active = !m_cancellationToken.IsCancelled; if (!m_active) return; FileEnumerator[] subdirectoryEnumerators = SubdirectoryEnumerators; if (index >= subdirectoryEnumerators.Length) { EnumerateNextFile(new EnumerableWrapper(EnumerateFiles(), m_cancellationToken)); return; } // Create a new thread for the subdirectory since this code is only invoked if the file // processor is using the enumeration strategy for processing subdirectories in parallel LogicalThread subdirectoryThread = m_fileProcessor.m_threadScheduler.CreateThread(); subdirectoryThread.Push(() => subdirectoryEnumerators[index].Enumerate(FileEnumerationStrategy.ParallelSubdirectories)); // Invoke the asynchronous call to enumerate the next directory LogicalThread.CurrentThread.Push(() => EnumerateNextDirectory(index + 1)); m_lastVisitedPath = subdirectoryEnumerators[index].Path; } catch { m_active = false; throw; } } // Enumerates the next file in the given enumerable. private void EnumerateNextFile(EnumerableWrapper wrapper) { try { m_active = AdvanceToNextMatch(wrapper); if (!m_active) return; ProcessCurrentFileAndEnumerateNextFile(wrapper); m_lastVisitedPath = wrapper.Current; } catch { m_active = false; wrapper.Dispose(); throw; } finally { if (!wrapper.LastMove) { m_active = false; wrapper.Dispose(); } } } // Advances enumeration until the next file that matches the filter. private bool AdvanceToNextMatch(EnumerableWrapper wrapper) { List skippedFiles = new List(); try { while (true) { if (!wrapper.MoveNext()) return false; if (m_fileProcessor.MatchesFilter(wrapper.Current)) return true; skippedFiles.Add(wrapper.Current); m_lastVisitedPath = wrapper.Current; } } finally { m_fileProcessor.m_processingThread.Push(1, () => { foreach (string filePath in skippedFiles) m_fileProcessor.TouchAndSkip(filePath); }); } } // Processes the current file in the given enumerable. private void ProcessCurrentFileAndEnumerateNextFile(EnumerableWrapper wrapper) { LogicalThread enumerationThread = LogicalThread.CurrentThread; string filePath = wrapper.Current; // Kick off the operation to process the current file, // but don't enumerate to the next file until processing // for the current file has begun m_fileProcessor.m_processingThread.Push(1, () => { // Check the state of cancellation for the // enumeration thread on the processing // thread as well to speed up cancellation if (m_cancellationToken.IsCancelled) { m_active = false; wrapper.Dispose(); return; } enumerationThread.Push(() => EnumerateNextFile(wrapper)); m_fileProcessor.TouchAndProcess(filePath, false); }); } private IEnumerable EnumerateDirectories() { IEnumerable directoryEnumerable = Directory.EnumerateDirectories(Path); return m_fileProcessor.OrderedEnumeration ? directoryEnumerable.OrderBy(dir => dir) : directoryEnumerable; } private IEnumerable EnumerateFiles() { IEnumerable fileEnumerable = Directory.EnumerateFiles(Path); return m_fileProcessor.OrderedEnumeration ? fileEnumerable.OrderBy(dir => dir) : fileEnumerable; } private void BuildActivelyVisitedPaths(List activelyVisitedPaths) { string lastVisitedPath = m_lastVisitedPath; if (m_active && (object)lastVisitedPath != null) activelyVisitedPaths.Add(lastVisitedPath); FileEnumerator[] subdirectoryEnumerators = SubdirectoryEnumerators; foreach (FileEnumerator enumerator in subdirectoryEnumerators) enumerator.BuildActivelyVisitedPaths(activelyVisitedPaths); } #endregion } private class EnumerableWrapper : IDisposable { #region [ Members ] // Fields private IEnumerable m_enumerable; private IEnumerator m_enumerator; private bool m_lastMove; private string m_current; private CancellationToken m_cancellationToken; private bool m_disposed; #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// /// The enumerable to be wrapped. /// The token used to cancel the enumeration operation. public EnumerableWrapper(IEnumerable enumerable, CancellationToken cancellationToken) { if ((object)enumerable == null) throw new ArgumentNullException(nameof(enumerable)); m_enumerable = enumerable; m_enumerator = enumerable.GetEnumerator(); m_cancellationToken = cancellationToken; } #endregion #region [ Properties ] /// /// Gets the value returned by the /// last call to . /// public bool LastMove { get { return m_lastMove; } } /// /// Gets the current value of the enumerator. /// public string Current { get { return m_current; } } /// /// Gets the cancellation token for this enumerator. /// public CancellationToken CancellationToken { get { return m_cancellationToken; } } #endregion #region [ Methods ] /// /// Advances the enumerator to the next element of the collection. /// /// True if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection. public bool MoveNext() { m_lastMove = (!m_cancellationToken.IsCancelled) && m_enumerator.MoveNext(); m_current = m_lastMove ? m_enumerator.Current : null; return m_lastMove; } /// /// Releases all the resources used by the enumerable. /// public void Dispose() { if (!m_disposed) { try { (m_enumerator as IDisposable)?.Dispose(); (m_enumerable as IDisposable)?.Dispose(); } finally { m_lastMove = false; m_current = null; m_disposed = true; } } } #endregion } // Constants /// /// Default value for the property. /// public const string DefaultFilter = @"**\*"; /// /// Default value for the property. /// public const bool DefaultTrackChanges = false; /// /// Default value for the property. /// public static readonly string DefaultCachePath = Path.Combine(FilePath.GetCommonApplicationDataFolder(), "FileProcessors"); /// /// Default value for the property. /// public const int DefaultInternalBufferSize = 8192; /// /// Default value for the property. /// public const int DefaultMaxFragmentation = 10; /// /// Default value for the property. /// public const FileEnumerationStrategy DefaultEnumerationStrategy = FileEnumerationStrategy.ParallelSubdirectories; // Events /// /// Event triggered when a file is to be processed. /// public event EventHandler Processing; /// /// Event triggered when an unexpected error occurs during /// normal operation of the . /// public event EventHandler Error; // Fields private readonly Guid m_processorID; private string m_filter; private Func m_filterMethod; private bool m_trackChanges; private string m_cachePath; private int m_internalBufferSize; private int m_maxFragmentation; private FileEnumerationStrategy m_enumerationStrategy; private bool m_orderedEnumeration; private readonly object m_trackedDirectoriesLock; private readonly List m_trackedDirectories; private LogicalThread m_sequentialEnumerationThread; private LogicalThread m_cleanProcessedFilesThread; private LogicalThreadOperation m_cleanProcessedFilesOperation; private LogicalThreadScheduler m_threadScheduler; private LogicalThread m_processingThread; private LogicalThread m_watcherThread; private Timer m_fileWatchTimer; private ManualResetEvent m_waitObject; private readonly Dictionary m_touchedFiles; private FileBackedHashSet m_processedFiles; private int m_processedFileCount; private int m_skippedFileCount; private int m_requeuedFileCount; private DateTime m_lastCompactTime; private TimeSpan m_lastCompactDuration; private bool m_disposed; #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// /// Identifies the file processor so that it can locate its processed file cache. public FileProcessor(Guid processorID) { m_processorID = processorID; m_filter = DefaultFilter; m_filterMethod = filePath => true; m_trackChanges = DefaultTrackChanges; m_cachePath = DefaultCachePath; m_internalBufferSize = DefaultInternalBufferSize; m_maxFragmentation = DefaultMaxFragmentation; m_enumerationStrategy = DefaultEnumerationStrategy; m_trackedDirectoriesLock = new object(); m_trackedDirectories = new List(); m_threadScheduler = new LogicalThreadScheduler(2); m_threadScheduler.UnhandledException += (sender, args) => OnError(args.Argument); m_processingThread = m_threadScheduler.CreateThread(); m_watcherThread = m_threadScheduler.CreateThread(); m_sequentialEnumerationThread = m_threadScheduler.CreateThread(); m_cleanProcessedFilesThread = m_threadScheduler.CreateThread(); m_cleanProcessedFilesOperation = new LogicalThreadOperation(m_cleanProcessedFilesThread, GetProcessedFiles, false); m_fileWatchTimer = new Timer(15000); m_fileWatchTimer.Elapsed += FileWatchTimer_Elapsed; m_waitObject = new ManualResetEvent(false); m_touchedFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); // As of .NET 4.7.1, the OrdinalIgnoreCase comparer produces a consistent hash code across 32-bit and 64-bit platforms, // but Microsoft provides no guarantees in terms of consistency across CLR versions. No testing has been done against Mono. // TODO: Consider implementing a platform-independent equality comparer for ignoring case, though it would be a breaking change. m_processedFiles = new FileBackedHashSet(Path.Combine(m_cachePath, m_processorID.ToString()), StringComparer.OrdinalIgnoreCase); // Fragmentation statistics are only valid for the lifetime of the process // so go ahead and compact the processed files collection for good measure Compact(); } #endregion #region [ Properties ] /// /// Gets or sets the filter used to determine whether a file should be processed. /// public string Filter { get { return m_filter; } set { m_filter = value ?? DefaultFilter; } } /// /// Gets or sets the user-defined method by which to filter /// files before raising the event. /// public Func FilterMethod { get { return m_filterMethod; } set { m_filterMethod = value ?? (filePath => true); } } /// /// Gets or sets a flag that determines whether files should be processed on change. /// public bool TrackChanges { get { return m_trackChanges; } set { m_trackChanges = value; } } /// /// Gets or sets the path to the cache where the list of processed files will be persisted. /// public string CachePath { get { return m_cachePath; } set { lock (m_trackedDirectoriesLock) { if (m_trackedDirectories.Count > 0) throw new InvalidOperationException("File processor is already tracking directories - modification of the cache path would be unsafe."); if (m_cachePath != value) { if ((object)m_cachePath != null) m_cachePath = FilePath.GetAbsolutePath(value); else m_cachePath = DefaultCachePath; m_processedFiles.FilePath = Path.Combine(m_cachePath, m_processorID.ToString()); } } } } /// /// Gets or sets the internal buffer size of each of the /// s instantiated by this file processor. /// public int InternalBufferSize { get { return m_internalBufferSize; } set { m_internalBufferSize = value; } } /// /// Gets or sets the maximum number of threads /// used for file processing and enumeration. /// public int MaxThreadCount { get { return m_threadScheduler.MaxThreadCount; } set { m_threadScheduler.MaxThreadCount = value; } } /// /// Gets or sets the maximum amount of fragmentation allowed /// before compacting the lookup table for processed files. /// /// /// If files are frequently removed from the watch directories, /// increasing this value may improve performance. Setting the /// value too high may result in the file perpetually growing /// or long pauses on the processing thread. /// public int MaxFragmentation { get { return m_maxFragmentation; } set { m_maxFragmentation = value; } } /// /// Gets or sets the strategy to use to /// enumerate files in the watch directories. /// public FileEnumerationStrategy EnumerationStrategy { get { return m_enumerationStrategy; } set { m_enumerationStrategy = value; } } /// /// Gets or sets the flag that determines whether the file enumeration process /// should sort files and directories before raising events for enumerated files. /// public bool OrderedEnumeration { get { return m_orderedEnumeration; } set { m_orderedEnumeration = value; } } /// /// Gets the list of directories currently being tracked by the . /// public IList TrackedDirectories { get { lock (m_trackedDirectoriesLock) { return m_trackedDirectories .Select(dir => dir.Path) .ToList(); } } } /// /// Gets the list of paths that are being actively enumerated. /// public IList ActivelyEnumeratedPaths { get { lock (m_trackedDirectoriesLock) { return m_trackedDirectories .SelectMany(dir => dir.ActivelyEnumeratedPaths) .ToList(); } } } /// /// Gets the flag indicating if the file processor is actively enumerating. /// public bool IsEnumerating { get { lock (m_trackedDirectoriesLock) { return m_trackedDirectories.Any(dir => dir.IsEnumerating); } } } /// /// Gets the flag indicating if the file processor is actively /// purging processed files from the internal lookup table. /// public bool IsCleaning { get { return m_cleanProcessedFilesOperation.IsRunning; } } /// /// Gets the number of files /// processed by the file processor. /// public int ProcessedFileCount { get { return Interlocked.CompareExchange(ref m_processedFileCount, 0, 0); } } /// /// Gets the number of files skipped by the file processor. /// public int SkippedFileCount { get { return Interlocked.CompareExchange(ref m_skippedFileCount, 0, 0); } } /// /// Gets the number of times files that are being requeued by the file processor. /// public int RequeuedFileCount { get { return Interlocked.CompareExchange(ref m_requeuedFileCount, 0, 0); } } /// /// Gets the time at which the last operation to /// compact the set of processed files occurred. /// public DateTime LastCompactTime { get { return m_lastCompactTime; } } /// /// Gets the amount of time spent during the last /// operation to compact the set of processed files. /// public TimeSpan LastCompactDuration { get { return m_lastCompactDuration; } } #endregion #region [ Methods ] /// /// Adds a directory to the list of directories tracked by this . /// /// The path to the directory to be tracked. public void AddTrackedDirectory(string path) { if (m_disposed) throw new ObjectDisposedException(nameof(FileProcessor)); string fullPath = FilePath.GetAbsolutePath(path); TrackedDirectory trackedDirectory; lock (m_trackedDirectoriesLock) { if (TrackedDirectories.Contains(fullPath)) return; if (m_trackedDirectories.Count == 0) m_fileWatchTimer.Start(); trackedDirectory = new TrackedDirectory(this, fullPath); m_trackedDirectories.Add(trackedDirectory); } FileEnumerationStrategy enumerationStrategy = m_enumerationStrategy; LogicalThread enumerationThread = (enumerationStrategy == FileEnumerationStrategy.Sequential) ? m_sequentialEnumerationThread : m_threadScheduler.CreateThread(); enumerationThread.Push(() => trackedDirectory.EnumerateFiles(enumerationStrategy)); } /// /// Removes a directory from the list of directories tracked by this . /// /// The path to the directory to stop tracking. public void RemoveTrackedDirectory(string path) { if (m_disposed) throw new ObjectDisposedException(nameof(FileProcessor)); string fullPath = FilePath.GetAbsolutePath(path); lock (m_trackedDirectoriesLock) { List trackedDirectoriesToRemove = m_trackedDirectories .Where(dir => fullPath.Equals(dir.Path, StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (TrackedDirectory trackedDirectory in trackedDirectoriesToRemove) { m_trackedDirectories.Remove(trackedDirectory); trackedDirectory.Dispose(); } if (m_trackedDirectories.Count == 0) { m_fileWatchTimer.Stop(); m_processedFiles.Close(); } } } /// /// Forces enumeration of directories currently being watched. /// public void EnumerateWatchDirectories() { if (m_disposed) throw new ObjectDisposedException(nameof(FileProcessor)); List trackedDirectories; lock (m_trackedDirectoriesLock) { trackedDirectories = new List(m_trackedDirectories); } FileEnumerationStrategy enumerationStrategy = m_enumerationStrategy; LogicalThread GetThread() { return (enumerationStrategy == FileEnumerationStrategy.Sequential) ? m_sequentialEnumerationThread : m_threadScheduler.CreateThread(); } foreach (TrackedDirectory trackedDirectory in trackedDirectories) { if (!trackedDirectory.IsEnumerating) GetThread().Push(() => trackedDirectory.EnumerateFiles(enumerationStrategy)); } m_cleanProcessedFilesOperation.RunOnceAsync(); } /// /// Determines if the given file matches the filter string provided through the property. /// /// The path to the file to be tested against the filter string. /// True if the file matches the filter string; false otherwise. public bool MatchesFilter(string filePath) { if (m_disposed) throw new ObjectDisposedException(nameof(FileProcessor)); string[] filters = m_filter.Split(Path.PathSeparator); return FilePath.IsFilePatternMatch(filters, filePath, true); } /// /// Determines if the given file matches the filter method provided through the property. /// /// The path to the file to be tested against the filter method. /// True if the file matches the filter method; false otherwise. public bool MatchesFilterMethod(string filePath) { if (m_disposed) throw new ObjectDisposedException(nameof(FileProcessor)); try { return m_filterMethod(filePath); } catch (Exception ex) { string message = $"An exception occurred while attempting to execute user-defined filter: {ex.Message}"; OnError(new Exception(message, ex)); return false; } } /// /// Stops all enumeration operations that are currently running. /// public void StopEnumeration() { if (m_disposed) throw new ObjectDisposedException(nameof(FileProcessor)); lock (m_trackedDirectoriesLock) { foreach (TrackedDirectory trackedDirectory in m_trackedDirectories) trackedDirectory.CancelEnumeration(); } } /// /// Empties the list of directories tracked by this . /// public void ClearTrackedDirectories() { if (m_disposed) throw new ObjectDisposedException(nameof(FileProcessor)); lock (m_trackedDirectoriesLock) { while (m_trackedDirectories.Count > 0) RemoveTrackedDirectory(m_trackedDirectories[0].Path); } } /// /// Releases all the resources used by the object. /// [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "m_waitObject", Justification = "Thread pool threads could still be using this object.")] public void Dispose() { if (!m_disposed) { try { StopEnumeration(); ClearTrackedDirectories(); if ((object)m_fileWatchTimer != null) { m_fileWatchTimer.Dispose(); m_fileWatchTimer = null; } if ((object)m_processedFiles != null) { m_processedFiles.Dispose(); m_processedFiles = null; } if ((object)m_waitObject != null) { // DO NOT dispose of the wait object // We have to rely on the garbage collector to reclaim this // one or else we risk unhandled exceptions on the thread pool m_waitObject.Set(); m_waitObject = null; } } finally { m_disposed = true; // Prevent duplicate dispose. } } } // Checks and updates the touchedFiles lookup table, then starts the processing loop. private void TouchAndProcess(string filePath, bool raisedByFileWatcher) { DateTime lastWriteTime = File.GetLastWriteTimeUtc(filePath); DateTime lastKnownWriteTime; if (!m_touchedFiles.TryGetValue(filePath, out lastKnownWriteTime) || lastKnownWriteTime < lastWriteTime) { m_touchedFiles[filePath] = lastWriteTime; if (!MatchesFilterMethod(filePath)) { Interlocked.Increment(ref m_skippedFileCount); return; } StartProcessLoop(filePath, raisedByFileWatcher); } } // Checks and updates the touchedFiles lookup table, then increments the skipped file count. private void TouchAndSkip(string filePath) { DateTime lastWriteTime = File.GetLastWriteTimeUtc(filePath); DateTime lastKnownWriteTime; if (!m_touchedFiles.TryGetValue(filePath, out lastKnownWriteTime) || lastKnownWriteTime < lastWriteTime) { m_touchedFiles[filePath] = lastWriteTime; Interlocked.Increment(ref m_skippedFileCount); } } // Starts a loop to process the file. // Continues looping so long as the user continues requesting to requeue. private void StartProcessLoop(string filePath, bool raisedByFileWatcher) { ManualResetEvent waitObject = m_waitObject; if ((object)waitObject == null) return; bool alreadyProcessed = m_processedFiles.Contains(filePath); int retryCount = 0; Func retryCounter = () => retryCount; FileProcessorEventArgs args = new FileProcessorEventArgs(filePath, alreadyProcessed, raisedByFileWatcher, retryCounter); Action delayAndProcess = null; // 8 * 250 ms = 2 sec (cumulative: 2 sec) const int FastRetryLimit = 8; const int FastRetryDelay = 250; // 13 * 1000 ms = 13 sec (cumulative: 15 sec) const int QuickRetryLimit = 13 + FastRetryLimit; const int QuickRetryDelay = 1000; // 9 * 5000 ms = 45 sec (cumulative: 60 sec) const int RelaxedRetryLimit = 9 + QuickRetryLimit; const int RelaxedRetryDelay = 5000; // After 60 seconds, continue with the slow retry delay const int SlowRetryDelay = 60000; Action reprocessAction = () => { ProcessFile(args); if (args.Requeue) { int delay; if (retryCount < FastRetryLimit) delay = FastRetryDelay; else if (retryCount < QuickRetryLimit) delay = QuickRetryDelay; else if (retryCount < RelaxedRetryLimit) delay = RelaxedRetryDelay; else delay = SlowRetryDelay; delayAndProcess.DelayAndExecute(waitObject, delay); return; } Interlocked.Decrement(ref m_requeuedFileCount); }; delayAndProcess = () => { int priority = (++retryCount < RelaxedRetryLimit) ? 2 : 1; m_processingThread.Push(priority, reprocessAction); }; ProcessFile(args); if (!args.Requeue) return; Interlocked.Increment(ref m_requeuedFileCount); delayAndProcess.DelayAndExecute(waitObject, 250); } // Attempts to processes the given file. // Returns true if the user requested to requeue the file. private void ProcessFile(FileProcessorEventArgs args) { string filePath = args.FullPath; // Requeue is requested by the user so // we must reset it before raising the event args.Requeue = false; if (m_disposed || !File.Exists(filePath)) return; OnProcessing(args); // If the user requests to requeue the file, // we don't consider the file processed yet if (args.Requeue) return; Interlocked.Increment(ref m_processedFileCount); if (!args.AlreadyProcessed) m_processedFiles.Add(filePath); } // Gets the list of processed files from the file processor // and passes it to the cleaning thread for filtering. private void GetProcessedFiles() { m_processingThread.Push(1, () => m_cleanProcessedFilesOperation.ExecuteAction(() => { string[] processedFiles = m_processedFiles.ToArray(); FilterProcessedFiles(processedFiles); })); } // Filters the list of processed files to only the files that no longer // exist, then passes them back to the processing thread for removal. private void FilterProcessedFiles(string[] processedFiles) { m_cleanProcessedFilesThread.Push(() => m_cleanProcessedFilesOperation.ExecuteAction(() => { IList trackedDirectories = TrackedDirectories; string[] files = processedFiles .Where(file => trackedDirectories.Any(dir => file.StartsWith(dir, StringComparison.OrdinalIgnoreCase))) .Where(file => !File.Exists(file)) .ToArray(); RemoveProcessedFiles(files); })); } // Removes the list of files that no longer exist, // then deactivates the cleaning thread. private void RemoveProcessedFiles(string[] files) { m_processingThread.Push(1, () => m_cleanProcessedFilesOperation.ExecuteAction(() => { foreach (string file in files) { m_touchedFiles.Remove(file); m_processedFiles.Remove(file); } m_cleanProcessedFilesOperation.RunIfPending(); })); } // Defragments the lookup table to reduce disk space usage. private void Compact() { if (LogicalThread.CurrentThread != m_processingThread) { m_processingThread.Push(Compact); return; } DateTime lastCompactTime = DateTime.UtcNow; m_processedFiles.Compact(); m_lastCompactTime = lastCompactTime; m_lastCompactDuration = DateTime.UtcNow - m_lastCompactTime; } // Triggers the processing event for the given file. private void OnProcessing(FileProcessorEventArgs args) { Processing?.Invoke(this, args); } // Triggers the error event with the given exception. private void OnError(Exception ex) { if ((object)Error != null) Error(this, new ErrorEventArgs(ex)); } // Queues the created file for processing. private void Watcher_Created(object sender, FileSystemEventArgs args) { string fullPath = args.FullPath; m_watcherThread.Push(() => { if (!MatchesFilter(fullPath)) { m_processingThread.Push(1, () => TouchAndSkip(fullPath)); return; } m_processingThread.Push(2, () => TouchAndProcess(fullPath, true)); }); } // If the watcher tracks changes, queues the changed file for processing. private void Watcher_Changed(object sender, FileSystemEventArgs args) { if (!m_trackChanges) return; string fullPath = args.FullPath; m_watcherThread.Push(() => { if (!MatchesFilter(fullPath)) { m_processingThread.Push(1, () => TouchAndSkip(fullPath)); return; } m_processingThread.Push(2, () => { m_touchedFiles.Remove(fullPath); TouchAndProcess(fullPath, true); }); }); } // Track renames so that files whose names are changed can be updated in the processed files list. private void Watcher_Renamed(object sender, RenamedEventArgs args) { string oldFullPath = args.OldFullPath; string fullPath = args.FullPath; m_watcherThread.Push(() => { bool matchesFilter = MatchesFilter(fullPath); int priority = matchesFilter ? 2 : 1; m_processingThread.Push(priority, () => { if (m_touchedFiles.TryGetValue(oldFullPath, out DateTime lastWriteTime)) { m_touchedFiles.Remove(oldFullPath); m_touchedFiles.Add(fullPath, lastWriteTime); } if (m_processedFiles.Remove(oldFullPath)) m_processedFiles.Add(fullPath); if (matchesFilter) TouchAndProcess(fullPath, true); else TouchAndSkip(fullPath); }); }); } // Track deletes so that files can be removed from the processed files list. private void Watcher_Deleted(object sender, FileSystemEventArgs args) { string fullPath = args.FullPath; m_processingThread.Push(2, () => { m_touchedFiles.Remove(fullPath); m_processedFiles.Remove(fullPath); }); } // Triggers the error event for the exception encountered by the file watcher. private void Watcher_Error(object sender, ErrorEventArgs args) { if ((object)Error != null) Error(sender, args); } // Picks up files that were missed by the file watchers. private void FileWatchTimer_Elapsed(object sender, ElapsedEventArgs elapsedEventArgs) { // Check each of the existing file watchers to determine whether an // error occurred that forced the file watcher to stop raising events lock (m_trackedDirectoriesLock) { foreach (TrackedDirectory trackedDirectory in m_trackedDirectories) { try { trackedDirectory.CheckFileWatcher(); } catch (Exception ex) { OnError(ex); } } } // Determine if we need to defragment the lookup table for processed files m_processingThread.Push(1, () => { if (m_processedFiles.FragmentationCount > m_maxFragmentation) Compact(); }); } #endregion } }