//****************************************************************************************************** // 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; 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 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. public FileProcessorEventArgs(string fullPath, bool alreadyProcessed) { m_fullPath = fullPath; m_alreadyProcessed = alreadyProcessed; } #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 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 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 } private class FileEnumerator { #region [ Members ] // Fields private FileProcessor m_fileProcessor; private Dictionary m_cancellationTokens; private int m_enumerationThreads; private LogicalThread m_sequentialEnumerationThread; private LogicalThreadLocal m_isActive; private LogicalThreadLocal> m_wrapperStack; private LogicalThreadLocal> m_directoryQueue; private LogicalThread m_cleanProcessedFilesThread; private LogicalThreadOperation m_cleanProcessedFilesOperation; #endregion #region [ Properties ] /// /// Gets the number of active enumeration threads. /// public int EnumerationThreads { get { return Interlocked.CompareExchange(ref m_enumerationThreads, 0, 0); } } #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// /// The file processor that created the file enumerator. public FileEnumerator(FileProcessor fileProcessor) { m_fileProcessor = fileProcessor; m_cancellationTokens = new Dictionary(); m_sequentialEnumerationThread = m_fileProcessor.m_threadScheduler.CreateThread(); m_isActive = new LogicalThreadLocal(); m_wrapperStack = new LogicalThreadLocal>(() => new Stack()); m_directoryQueue = new LogicalThreadLocal>(() => new Queue()); m_cleanProcessedFilesThread = m_fileProcessor.m_threadScheduler.CreateThread(); m_cleanProcessedFilesOperation = new LogicalThreadOperation(m_cleanProcessedFilesThread, GetProcessedFiles, false); } #endregion #region [ Methods ] /// /// Initiates enumeration of the watch directories. /// /// The directory to be enumerated. public void Enumerate(string directory) { CancellationToken cancellationToken; lock (m_cancellationTokens) { if (m_cancellationTokens.TryGetValue(directory, out cancellationToken)) cancellationToken.Cancel(); cancellationToken = new CancellationToken(); m_cancellationTokens[directory] = cancellationToken; } switch (m_fileProcessor.EnumerationStrategy) { // Sequential enumeration strategy kicks // off all its processing on a single thread case FileEnumerationStrategy.Sequential: m_sequentialEnumerationThread.Push(() => { // Create the enumerable wrappers for file and directory enumeration EnumerableWrapper fileWrapper = new EnumerableWrapper(Directory.EnumerateFiles(directory), cancellationToken); EnumerableWrapper directoryWrapper = new EnumerableWrapper(Directory.EnumerateDirectories(directory), cancellationToken); if (m_isActive.Value) { // If the thread is already active, // push processing into the directory queue m_directoryQueue.Value.Enqueue(() => { m_wrapperStack.Value.Push(() => EnumerateNextFile(fileWrapper)); EnumerateNextDirectory(directoryWrapper); }); } else { // If the thread is inactive, mark it as active // and then begin processing the directory wrapper ActivateThread(); m_wrapperStack.Value.Push(() => EnumerateNextFile(fileWrapper)); EnumerateNextDirectory(directoryWrapper); } }); break; // Parallel processing kicks off new // enumerations on their own thread case FileEnumerationStrategy.ParallelWatchDirectories: case FileEnumerationStrategy.ParallelSubdirectories: m_fileProcessor.m_threadScheduler.CreateThread().Push(() => { EnumerableWrapper fileWrapper = new EnumerableWrapper(Directory.EnumerateFiles(directory), cancellationToken); EnumerableWrapper directoryWrapper = new EnumerableWrapper(Directory.EnumerateDirectories(directory), cancellationToken); m_wrapperStack.Value.Push(() => EnumerateNextFile(fileWrapper)); LogicalThread.CurrentThread.Push(() => EnumerateNextDirectory(directoryWrapper)); ActivateThread(); }); break; } // Initiate the process to clean up the files // in the collection of processed files m_cleanProcessedFilesOperation.RunOnceAsync(); } /// /// Cancels enumeration for a specific directory. /// /// The directory for which enumeration should be cancelled. public void Cancel(string directory) { CancellationToken cancellationToken; lock (m_cancellationTokens) { if (!m_cancellationTokens.TryGetValue(directory, out cancellationToken)) return; m_cancellationTokens.Remove(directory); } cancellationToken.Cancel(); } /// /// Cancels all running enumeration operations. /// public void Cancel() { CancellationToken[] cancellationTokens; lock (m_cancellationTokens) { cancellationTokens = m_cancellationTokens.Values.ToArray(); m_cancellationTokens.Clear(); } foreach (CancellationToken cancellationToken in cancellationTokens) cancellationToken.Cancel(); } // Handle the next directory in the enumeration. private void EnumerateNextDirectory(EnumerableWrapper wrapper) { EnumerableWrapper fileWrapper; EnumerableWrapper directoryWrapper; string directory; try { // Advance directory enumeration if (!wrapper.MoveNext()) { // No more directories, so dispose // and then move to the next wrapper wrapper.Dispose(); EnumerateNextWrapper(); return; } // Initialize the fileWrapper // and directoryWrapper directory = wrapper.Current; fileWrapper = null; directoryWrapper = null; } catch { // If an error occurs, dispose of the // wrapper and then move to the next wrapper wrapper.Dispose(); EnumerateNextWrapper(); throw; } try { switch (m_fileProcessor.EnumerationStrategy) { // Sequential and ParallelWatchDirectories strategies // place subdirectories on the current thread case FileEnumerationStrategy.Sequential: case FileEnumerationStrategy.ParallelWatchDirectories: // Create the fileWrapper and directoryWrapper objects fileWrapper = new EnumerableWrapper(Directory.EnumerateFiles(directory), wrapper.CancellationToken); directoryWrapper = new EnumerableWrapper(Directory.EnumerateDirectories(directory), wrapper.CancellationToken); // Push the current directory wrapper onto the stack m_wrapperStack.Value.Push(() => EnumerateNextDirectory(wrapper)); // Push the subdirectory's file wrapper onto the stack m_wrapperStack.Value.Push(() => EnumerateNextFile(fileWrapper)); // Continue enumeration with the directory wrapper for the subdirectory LogicalThread.CurrentThread.Push(() => EnumerateNextDirectory(directoryWrapper)); break; // ParallelSubdirectories strategy spawns new threads for subdirectories case FileEnumerationStrategy.ParallelSubdirectories: // Create the fileWrapper and directoryWrapper objects fileWrapper = new EnumerableWrapper(Directory.EnumerateFiles(directory), wrapper.CancellationToken); directoryWrapper = new EnumerableWrapper(Directory.EnumerateDirectories(directory), wrapper.CancellationToken); // Create a new thread, push the file wrapper onto the new thread's // stack, then enumerate the directory wrapper on the new thread m_fileProcessor.m_threadScheduler.CreateThread().Push(() => { ActivateThread(); m_wrapperStack.Value.Push(() => EnumerateNextFile(fileWrapper)); EnumerateNextDirectory(directoryWrapper); }); // Continue enumeration on this thread with the current directory wrapper LogicalThread.CurrentThread.Push(() => EnumerateNextDirectory(wrapper)); break; // The only other file enumeration // strategy is no strategy at all default: // Clear out the stack and queue, // then dispose of the wrapper m_wrapperStack.Value.Clear(); m_directoryQueue.Value.Clear(); wrapper.Dispose(); DeactivateThread(); break; } } catch { // If an exception occurs, dispose of the file // wrapper and directory wrapper, then continue // enumeration with the current directory wrapper if ((object)fileWrapper != null) fileWrapper.Dispose(); if ((object)directoryWrapper != null) directoryWrapper.Dispose(); LogicalThread.CurrentThread.Push(() => EnumerateNextDirectory(wrapper)); throw; } } // Handle the next file in the enumeration. private void EnumerateNextFile(EnumerableWrapper wrapper) { LogicalThread enumerationThread; Action enumerateNextFile; string file; try { while (true) { // Advance enumeration until the // next file that matches the filter if (!wrapper.MoveNext()) return; if (m_fileProcessor.MatchesFilter(wrapper.Current)) break; Interlocked.Increment(ref m_fileProcessor.m_skippedFileCount); } // Prepare the callback to return execution to this thread enumerationThread = LogicalThread.CurrentThread; enumerateNextFile = () => EnumerateNextFile(wrapper); file = 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 (wrapper.CancellationToken.IsCancelled) { enumerationThread.Push(DeactivateThread); return; } enumerationThread.Push(enumerateNextFile); m_fileProcessor.TouchLockAndProcess(file); }); } catch { // If an exception occurs, // dispose of the enumerable wrapper wrapper.Dispose(); throw; } finally { // If there are no more files to enumerate, // dispose of the wrapper and move to the next wrapper if (!wrapper.LastMove) { wrapper.Dispose(); EnumerateNextWrapper(); } } } // Advances to processing the next wrapper in the stack. // If the stack is empty, processes the next wrapper in the queue. // If there are no wrappers left, deactivate the thread. private void EnumerateNextWrapper() { LogicalThread currentThread = LogicalThread.CurrentThread; if (m_wrapperStack.Value.Count > 0) currentThread.Push(m_wrapperStack.Value.Pop()); else if (m_directoryQueue.Value.Count > 0) currentThread.Push(m_directoryQueue.Value.Dequeue()); else DeactivateThread(); } // Activates the thread and increments // the number of active threads. private void ActivateThread() { m_isActive.Value = true; Interlocked.Increment(ref m_enumerationThreads); } // Deactivates the thread and decrements // the number of active threads. private void DeactivateThread() { Interlocked.Decrement(ref m_enumerationThreads); m_isActive.Value = false; } // Gets the list of processed files from the file processor // and passes it to the cleaning thread for filtering. private void GetProcessedFiles() { m_fileProcessor.m_processingThread.Push(1, () => m_cleanProcessedFilesOperation.ExecuteAction(() => { string[] processedFiles = m_fileProcessor.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 = m_fileProcessor.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_fileProcessor.m_processingThread.Push(1, () => m_cleanProcessedFilesOperation.ExecuteAction(() => { foreach (string file in files) m_fileProcessor.m_processedFiles.Remove(file); m_cleanProcessedFilesOperation.RunIfPending(); })); } #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 readonly object m_fileWatchersLock; private readonly List m_fileWatchers; private readonly FileEnumerator m_enumerator; 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_fileWatchersLock = new object(); m_fileWatchers = new List(); m_threadScheduler = new LogicalThreadScheduler(); m_threadScheduler.UnhandledException += (sender, args) => OnError(args.Argument); m_processingThread = m_threadScheduler.CreateThread(2); m_watcherThread = m_threadScheduler.CreateThread(); m_fileWatchTimer = new Timer(15000); m_fileWatchTimer.Elapsed += FileWatchTimer_Elapsed; m_waitObject = new ManualResetEvent(false); m_touchedFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); m_processedFiles = new FileBackedHashSet(Path.Combine(m_cachePath, m_processorID.ToString()), StringComparer.OrdinalIgnoreCase); // Create the enumerator last since we are passing // a reference to 'this' into its constructor m_enumerator = new FileEnumerator(this); } #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_fileWatchers) { if (m_fileWatchers.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 the list of directories currently being tracked by the . /// public IList TrackedDirectories { get { lock (m_fileWatchers) { return m_fileWatchers .Select(watcher => watcher.Path) .ToList(); } } } /// /// Gets the number of enumeration threads currently running. /// public int EnumerationThreads { get { return m_enumerator.EnumerationThreads; } } /// /// 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) { string fullPath = FilePath.GetAbsolutePath(path); SafeFileWatcher watcher; if (!TrackedDirectories.Contains(fullPath, StringComparer.OrdinalIgnoreCase)) { watcher = new SafeFileWatcher(fullPath); watcher.IncludeSubdirectories = true; watcher.InternalBufferSize = m_internalBufferSize; watcher.Created += Watcher_Created; watcher.Changed += Watcher_Changed; watcher.Renamed += Watcher_Renamed; watcher.Deleted += Watcher_Deleted; watcher.Error += Watcher_Error; lock (m_fileWatchersLock) { if (m_fileWatchers.Count == 0) { m_processingThread.Push(2, LoadProcessedFiles); m_fileWatchTimer.Start(); } m_fileWatchers.Add(watcher); } watcher.EnableRaisingEvents = true; m_enumerator.Enumerate(fullPath); } } /// /// Removes a directory from the list of directories tracked by this . /// /// The path to the directory to stop tracking. public void RemoveTrackedDirectory(string path) { string fullPath = FilePath.GetAbsolutePath(path); List fileWatchersToRemove; lock (m_fileWatchers) { fileWatchersToRemove = m_fileWatchers .Where(w => fullPath.Equals(w.Path, StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (SafeFileWatcher watcher in fileWatchersToRemove) RemoveFileWatcher(watcher); if (m_fileWatchers.Count == 0) { m_fileWatchTimer.Stop(); m_processedFiles.Close(); } } m_enumerator.Cancel(path); } /// /// Forces enumeration of directories currently being watched. /// public void EnumerateWatchDirectories() { lock (m_fileWatchers) { foreach (SafeFileWatcher fileWatcher in m_fileWatchers) m_enumerator.Enumerate(fileWatcher.Path); } } /// /// Stops all enumeration operations that are currently running. /// public void StopEnumeration() { m_enumerator.Cancel(); } /// /// Empties the list of directories tracked by this . /// public void ClearTrackedDirectories() { lock (m_fileWatchers) { while (m_fileWatchers.Count > 0) RemoveFileWatcher(m_fileWatchers[0]); } } /// /// Releases all the resources used by the object. /// public void Dispose() { if (!m_disposed) { try { m_enumerator.Cancel(); 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) { m_waitObject.Set(); m_waitObject.Dispose(); m_waitObject = null; } } finally { m_disposed = true; // Prevent duplicate dispose. } } } // Determines if the given file matches the file processor's filter. private bool MatchesFilter(string filePath) { string[] filters = m_filter.Split(Path.PathSeparator); if (!FilePath.IsFilePatternMatch(filters, filePath, true)) return false; 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; } } // Queues the file for processing. private void QueueFileForProcessing(string filePath) { if (!MatchesFilter(filePath)) { Interlocked.Increment(ref m_skippedFileCount); return; } m_processingThread.Push(2, () => TouchLockAndProcess(filePath)); } // Checks and updates the touchedFiles lookup table, then calls LockAndProcess. private void TouchLockAndProcess(string filePath) { DateTime lastWriteTime = File.GetLastWriteTimeUtc(filePath); DateTime lastKnownWriteTime; if (!m_touchedFiles.TryGetValue(filePath, out lastKnownWriteTime) || lastKnownWriteTime < lastWriteTime) { m_touchedFiles[filePath] = lastWriteTime; StartProcessLoop(filePath); } } // Starts a loop to process the file. // Continues looping so long as the user continues requesting to requeue. private void StartProcessLoop(string filePath) { int retryCount = 0; Action delayAndProcess = null; delayAndProcess = () => { int priority = (++retryCount < 32) ? 2 : 1; m_processingThread.Push(priority, () => { int delay; if (ProcessFile(filePath)) { if (retryCount < 8) delay = 250; else if (retryCount < 23) delay = 1000; else if (retryCount < 32) delay = 5000; else delay = 60000; DelayAndExecute(delayAndProcess, delay); return; } Interlocked.Decrement(ref m_requeuedFileCount); }); }; if (ProcessFile(filePath)) { Interlocked.Increment(ref m_requeuedFileCount); DelayAndExecute(delayAndProcess, 250); } } // Attempts to processes the given file. // Returns true if the user requested to requeue the file. private bool ProcessFile(string filePath) { bool alreadyProcessed; // If the file processor is disposed or the // file no longer exists, return immediately if (m_disposed || !File.Exists(filePath)) return false; // Process the file at the given file path alreadyProcessed = m_processedFiles.Contains(filePath); // If the user requests to requeue the file, return true if (OnProcessing(filePath, alreadyProcessed)) return true; Interlocked.Increment(ref m_processedFileCount); // Update the list of processed files // and save it back to the cache if (!alreadyProcessed) m_processedFiles.Add(filePath); return false; } // Executes the given action after waiting the given number of milliseconds. private void DelayAndExecute(Action action, int delay) { object waitHandleLock = new object(); RegisteredWaitHandle waitHandle = null; WaitOrTimerCallback callback = (state, timeout) => { if (Interlocked.Exchange(ref waitHandleLock, null) == null) waitHandle.Unregister(null); if (!timeout) return; action(); }; waitHandle = ThreadPool.RegisterWaitForSingleObject(m_waitObject, callback, null, delay, true); if (Interlocked.Exchange(ref waitHandleLock, null) == null) waitHandle.Unregister(null); } // Loads the list of processed files from the cache. private void LoadProcessedFiles() { try { string cachePath = Path.Combine(m_cachePath, m_processorID.ToString()); List processedFilesList; byte[] signature; if (File.Exists(cachePath)) { processedFilesList = new List(); signature = m_processedFiles.DefaultSignature; using (FileStream stream = File.OpenRead(cachePath)) { // Read the signature from the start of the file if (stream.Read(signature, 0, signature.Length) <= 0) return; // Compare the signature to the default signature of the processedFiles hash set if (signature.SequenceEqual(m_processedFiles.DefaultSignature)) return; // Signature of the file does not match that of the m_processedFiles hash set so // assume this is a text file with a list of the files that have been processed stream.Position = 0; // Read each line of the file into a list to // be consumed by the m_processedFiles hash set using (StreamReader reader = new StreamReader(stream, Encoding.Unicode)) { while (!reader.EndOfStream) { string fullPath = reader.ReadLine(); if (!string.IsNullOrEmpty(fullPath)) processedFilesList.Add(fullPath); } } } // Delete the existing file since it // does not have the right signature File.Delete(cachePath); // Copy files that we read from the text file // into the m_processedFiles hash set foreach (string fullPath in processedFilesList) m_processedFiles.Add(fullPath); } } catch (Exception ex) { string message = string.Format("Unable to load processed files cache due to exception: {0}", ex.Message); OnError(new InvalidOperationException(message, ex)); } } // Detaches from events, removes the given file watcher from // the list of watchers, and disposes of the file watcher. private void RemoveFileWatcher(SafeFileWatcher watcher) { watcher.Created -= Watcher_Created; watcher.Changed -= Watcher_Changed; watcher.Renamed -= Watcher_Renamed; watcher.Deleted -= Watcher_Deleted; watcher.Error -= Watcher_Error; m_fileWatchers.Remove(watcher); watcher.Dispose(); } // Triggers the processing event for the given file. private bool OnProcessing(string fullPath, bool alreadyProcessed) { FileProcessorEventArgs args; if ((object)Processing != null) { args = new FileProcessorEventArgs(fullPath, alreadyProcessed); Processing(this, args); return args.Requeue; } return false; } // 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) { m_watcherThread.Push(() => { if (!MatchesFilter(args.FullPath)) { Interlocked.Increment(ref m_skippedFileCount); return; } m_processingThread.Push(2, () => TouchLockAndProcess(args.FullPath)); }); } // If the watcher tracks changes, queues the changed file for processing. private void Watcher_Changed(object sender, FileSystemEventArgs args) { if (!m_trackChanges) return; m_watcherThread.Push(() => { if (MatchesFilter(args.FullPath)) { m_processingThread.Push(2, () => { m_touchedFiles.Remove(args.FullPath); TouchLockAndProcess(args.FullPath); }); } else { Interlocked.Increment(ref m_skippedFileCount); } }); } // 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) { m_watcherThread.Push(() => { bool oldMatch = MatchesFilter(args.OldFullPath); bool newMatch = MatchesFilter(args.FullPath); if (!oldMatch && !newMatch) return; m_processingThread.Push(2, () => { if (oldMatch && m_touchedFiles.Remove(args.OldFullPath) && newMatch) m_touchedFiles.Add(args.FullPath, File.GetLastWriteTimeUtc(args.FullPath)); if (oldMatch && m_processedFiles.Remove(args.OldFullPath)) m_processedFiles.Add(args.FullPath); if (!oldMatch && newMatch) TouchLockAndProcess(args.FullPath); }); }); } // Track deletes so that files can be removed from the processed files list. private void Watcher_Deleted(object sender, FileSystemEventArgs args) { m_watcherThread.Push(() => { if (!MatchesFilter(args.FullPath)) return; m_processingThread.Push(2, () => { m_touchedFiles.Remove(args.FullPath); m_processedFiles.Remove(args.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_fileWatchers) { for (int i = 0; i < m_fileWatchers.Count; i++) { if (!m_fileWatchers[i].EnableRaisingEvents) { try { // This file watcher is no longer raising events so // attempt to create a new file watcher for that file path SafeFileWatcher newWatcher = new SafeFileWatcher(m_fileWatchers[i].Path); newWatcher.IncludeSubdirectories = true; newWatcher.Created += Watcher_Created; newWatcher.Changed += Watcher_Changed; newWatcher.Renamed += Watcher_Renamed; newWatcher.Deleted += Watcher_Deleted; newWatcher.Error += Watcher_Error; m_fileWatchers[i].Created -= Watcher_Created; m_fileWatchers[i].Changed -= Watcher_Changed; m_fileWatchers[i].Renamed -= Watcher_Renamed; m_fileWatchers[i].Deleted -= Watcher_Deleted; m_fileWatchers[i].Error -= Watcher_Error; m_fileWatchers[i].Dispose(); m_fileWatchers[i] = newWatcher; newWatcher.EnableRaisingEvents = true; // Files may have been dropped or removed while the file watcher // was disconnected so we need to enumerate the files again m_enumerator.Enumerate(newWatcher.Path); } 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) { DateTime lastCompactTime = DateTime.UtcNow; m_processedFiles.Compact(); m_lastCompactTime = lastCompactTime; m_lastCompactDuration = m_lastCompactTime - DateTime.UtcNow; } }); } #endregion } }