//****************************************************************************************************** // 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.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Timers; 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_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 event was raised by the file watcher. /// The function that provides the value for . public FileProcessorEventArgs(string fullPath, bool raisedByFileWatcher, Func retryCounter) { m_fullPath = fullPath; 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 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 ] // Events private event EventHandler>> ActivelyVisitedPathsRequested; // Fields private readonly FileProcessor m_fileProcessor; private SafeFileWatcher m_fileWatcher; private CancellationToken m_cancellationToken; 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 List ActivelyEnumeratedPaths { get { List activelyVisitedPaths = new List(); EventArgs> args = new EventArgs>(activelyVisitedPaths); ActivelyVisitedPathsRequested?.Invoke(this, args); return activelyVisitedPaths; } } #endregion #region [ Methods ] public async Task EnumerateAsync(FileEnumerationStrategy fileEnumerationStrategy) { if (m_disposed) return; CancellationToken cancellationToken = new CancellationToken(); Interlocked.Exchange(ref m_cancellationToken, cancellationToken); DirectoryInfo directory = new DirectoryInfo(Path); await EnumerateDirectoryAsync(directory, fileEnumerationStrategy, cancellationToken); } public void CancelEnumeration() { CancellationToken cancellationToken = Interlocked.CompareExchange(ref m_cancellationToken, null, null); cancellationToken?.Cancel(); } public void CheckFileWatcher() { if (m_disposed) return; if (!m_fileWatcher.EnableRaisingEvents) { DisposeFileWatcher(); CreateFileWatcher(); } } public void Dispose() { if (m_disposed) return; try { CancelEnumeration(); DisposeFileWatcher(); } finally { m_disposed = true; } } private async Task EnumerateDirectoryAsync(DirectoryInfo directory, FileEnumerationStrategy fileEnumerationStrategy, CancellationToken cancellationToken) { bool parallelSubdirectories = fileEnumerationStrategy == FileEnumerationStrategy.ParallelSubdirectories; LogicalThread thread = LogicalThread.CurrentThread; if (parallelSubdirectories) { LogicalThreadScheduler scheduler = m_fileProcessor.m_threadScheduler; thread = scheduler.CreateThread(); await thread.Join(); } async Task ForEach(IEnumerable tasks) { foreach (Task task in tasks) { if (cancellationToken.IsCancelled) return; await task; await thread.Yield(); } } LogicalThread processingThread = m_fileProcessor.m_processingThread; string activePath = null; async Task VisitSubdirectoryAsync(DirectoryInfo subdirectory) { activePath = subdirectory.FullName; if (cancellationToken.IsCancelled) return; if (m_fileProcessor.MatchesFolderExclusion(subdirectory.FullName)) return; await EnumerateDirectoryAsync(subdirectory, fileEnumerationStrategy, cancellationToken); } async Task VisitFileAsync(FileInfo file) { activePath = file.FullName; if (cancellationToken.IsCancelled) return Task.CompletedTask; async Task InvokeOnProcessingThread(Action action) { await processingThread.Join(); action(); } DateTime lastWriteTime = file.LastWriteTimeUtc; void Process() => m_fileProcessor.TouchAndProcess(file.FullName, lastWriteTime, false); void Skip() => m_fileProcessor.TouchAndSkip(file.FullName, lastWriteTime); if (!m_fileProcessor.MatchesFilter(file.FullName)) return InvokeOnProcessingThread(Skip); Task processTask = InvokeOnProcessingThread(Process); await thread.Yield(); return processTask; } async Task EnumerateSubdirectoriesAsync() { Func, Task> whenAll = parallelSubdirectories ? Task.WhenAll : ForEach; IEnumerable subdirectoryTasks = FilePath .EnumerateDirectories(directory, "*", SearchOption.TopDirectoryOnly, m_fileProcessor.OnError) .Select(VisitSubdirectoryAsync); await whenAll(subdirectoryTasks); } async Task EnumerateFilesAsync() { IEnumerable> fileTasks = FilePath .EnumerateFiles(directory, "*", SearchOption.TopDirectoryOnly) .Select(VisitFileAsync); List processTasks = new List(); foreach (Task fileTask in fileTasks) processTasks.Add(await fileTask); await Task.WhenAll(processTasks); } EventHandler>> handler = (sender, args) => { if (!(activePath is null)) args.Argument.Add(activePath); }; try { ActivelyVisitedPathsRequested += handler; if (parallelSubdirectories) { Task subdirectoriesTask = EnumerateSubdirectoriesAsync(); await EnumerateFilesAsync(); ActivelyVisitedPathsRequested -= handler; await subdirectoriesTask; } else { await EnumerateFilesAsync(); await EnumerateSubdirectoriesAsync(); } } finally { ActivelyVisitedPathsRequested -= handler; } } 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 } // Constants /// /// Default value for the property. /// public const string DefaultFilter = @"**\*"; /// /// Default value for the property. /// public const string DefaultFolderExclusion = ""; /// /// Default value for the property. /// public const bool DefaultTrackChanges = false; /// /// Default value for the property. /// public const int DefaultInternalBufferSize = 8192; /// /// 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 string m_filter; private string m_folderExclusion; private bool m_trackChanges; private int m_internalBufferSize; private FileEnumerationStrategy m_enumerationStrategy; private bool m_orderedEnumeration; private readonly object m_trackedDirectoriesLock; private readonly List m_trackedDirectories; private readonly TaskSynchronizedOperation m_enumerationOperation; private readonly LogicalThreadScheduler m_threadScheduler; private readonly LogicalThread m_processingThread; private readonly LogicalThread m_watcherThread; private readonly LogicalThread m_sequentialEnumerationThread; private readonly Timer m_fileWatchTimer; private readonly ManagedCancellationTokenSource m_requeueTokenSource; private readonly Dictionary m_touchedFiles; private int m_processedFileCount; private int m_skippedFileCount; private int m_requeuedFileCount; private bool m_disposed; #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// public FileProcessor() { m_filter = DefaultFilter; m_folderExclusion = DefaultFolderExclusion; m_trackChanges = DefaultTrackChanges; m_internalBufferSize = DefaultInternalBufferSize; m_enumerationStrategy = DefaultEnumerationStrategy; m_trackedDirectoriesLock = new object(); m_trackedDirectories = new List(); m_enumerationOperation = new TaskSynchronizedOperation(EnumerateWatchDirectoriesAsync, OnError); 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_fileWatchTimer = new Timer(15000); m_fileWatchTimer.Elapsed += FileWatchTimer_Elapsed; m_requeueTokenSource = new ManagedCancellationTokenSource(); m_touchedFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); } #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 pattern used to determine whether a folder should be excluded from enumeration. /// public string FolderExclusion { get { return m_folderExclusion; } set { m_folderExclusion = value ?? DefaultFolderExclusion; } } /// /// 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 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 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 => m_enumerationOperation.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); } } #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); } } /// /// 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(); } } /// /// Forces enumeration of directories currently being watched. /// public void EnumerateWatchDirectories() { if (m_disposed) throw new ObjectDisposedException(nameof(FileProcessor)); m_enumerationOperation.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 folder matches the exclusion string provided through the property. /// /// The path to the folder to be tested against the exclusion string. /// True if the folder matches the exclusion string; false otherwise. public bool MatchesFolderExclusion(string folderPath) { if (m_disposed) throw new ObjectDisposedException(nameof(FileProcessor)); string folderPathWithoutSeparator = folderPath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); string folderPathWithSeparator = $"{folderPathWithoutSeparator}\\"; string[] patterns = m_folderExclusion.Split(Path.PathSeparator); return FilePath.IsFilePatternMatch(patterns, folderPathWithoutSeparator, true) || FilePath.IsFilePatternMatch(patterns, folderPathWithSeparator, true); } /// /// 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); } } /// /// Resets the internal file index so all files can be reprocessed. /// /// /// As an optimization, the internal file index allows the file processor to enumerate /// the folder without raising events for files that were already processed, enabling /// the file processor to process files that were missed by the file watcher. However, /// because of this optimization, it can instead be difficult to force the file processor /// to reprocess files it has already processed once before. This method clears the /// internal file index, allowing for reprocessing of files that have already been processed. /// public void ResetIndexAndStatistics() { if (m_disposed) throw new ObjectDisposedException(nameof(FileProcessor)); m_processingThread.Push(2, () => { m_touchedFiles.Clear(); Interlocked.Exchange(ref m_processedFileCount, 0); Interlocked.Exchange(ref m_skippedFileCount, 0); Interlocked.Exchange(ref m_requeuedFileCount, 0); }); } /// /// Releases all the resources used by the object. /// public void Dispose() { if (!m_disposed) { try { StopEnumeration(); ClearTrackedDirectories(); m_fileWatchTimer.Stop(); m_fileWatchTimer.Dispose(); m_requeueTokenSource.Dispose(); } finally { m_disposed = true; // Prevent duplicate dispose. } } } // Forces enumeration of directories currently being watched. private async Task EnumerateWatchDirectoriesAsync() { List GetTrackedDirectories() { lock (m_trackedDirectoriesLock) return new List(m_trackedDirectories); } List trackedDirectories = GetTrackedDirectories(); FileEnumerationStrategy enumerationStrategy = m_enumerationStrategy; bool sequential = enumerationStrategy == FileEnumerationStrategy.Sequential; Func getEnumerationThread = sequential ? () => m_sequentialEnumerationThread : () => m_threadScheduler.CreateThread(); async Task EnumerateTrackedDirectoryAsync(TrackedDirectory trackedDirectory) { try { LogicalThread enumerationThread = getEnumerationThread(); await enumerationThread.Join(); await trackedDirectory.EnumerateAsync(enumerationStrategy); } catch (Exception ex) { OnError(ex); } } IEnumerable enumerationTasks = trackedDirectories .Select(EnumerateTrackedDirectoryAsync); async Task ForEach(IEnumerable tasks) { foreach (Task task in tasks) await task; } Func, Task> whenAll = sequential ? ForEach : Task.WhenAll; await whenAll(enumerationTasks); } // Checks and updates the touchedFiles lookup table, then starts the processing loop. private void TouchAndProcess(string filePath, DateTime lastWriteTime, bool raisedByFileWatcher) { bool touched = m_touchedFiles.TryGetValue(filePath, out DateTime lastKnownWriteTime); async Task ProcessAsync() { await RunProcessLoopAsync(filePath, raisedByFileWatcher); if (!touched) Interlocked.Increment(ref m_processedFileCount); } if (!touched || lastKnownWriteTime < lastWriteTime) { m_touchedFiles[filePath] = lastWriteTime; _ = ProcessAsync(); } } // Checks and updates the touchedFiles lookup table, then increments the skipped file count. private void TouchAndSkip(string filePath, DateTime lastWriteTime) { bool touched = m_touchedFiles.TryGetValue(filePath, out DateTime lastKnownWriteTime); if (!touched || lastKnownWriteTime < lastWriteTime) { m_touchedFiles[filePath] = lastWriteTime; if (!touched) Interlocked.Increment(ref m_skippedFileCount); } } // Runs an asynchronous loop to process the file. // Continues looping so long as the user continues requesting to requeue. private async Task RunProcessLoopAsync(string filePath, bool raisedByFileWatcher) { int retryCount = 0; int RetryCounter() => retryCount; FileProcessorEventArgs args = new FileProcessorEventArgs(filePath, raisedByFileWatcher, RetryCounter); // 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; int GetDelay() { if (retryCount < FastRetryLimit) return FastRetryDelay; if (retryCount < QuickRetryLimit) return QuickRetryDelay; if (retryCount < RelaxedRetryLimit) return RelaxedRetryDelay; return SlowRetryDelay; } using (m_requeueTokenSource.RetrieveToken(out var cancellationToken)) { while (true) { if (cancellationToken.IsCancellationRequested) break; int priority = (retryCount < RelaxedRetryLimit) ? 2 : 1; await m_processingThread.Join(priority); if (cancellationToken.IsCancellationRequested) break; ProcessFile(args); if (!args.Requeue) break; if (retryCount == 0) Interlocked.Increment(ref m_requeuedFileCount); retryCount++; int delay = GetDelay(); try { await Task.Delay(delay, cancellationToken); } catch (TaskCanceledException) { break; } } if (retryCount > 0) Interlocked.Decrement(ref m_requeuedFileCount); } } // 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); } // 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) { Error?.Invoke(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(() => { DateTime lastWriteTime = File.GetLastWriteTimeUtc(fullPath); if (!MatchesFilter(fullPath)) { m_processingThread.Push(1, () => TouchAndSkip(fullPath, lastWriteTime)); return; } m_processingThread.Push(2, () => TouchAndProcess(fullPath, lastWriteTime, 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(() => { DateTime lastWriteTime = File.GetLastWriteTimeUtc(fullPath); if (!MatchesFilter(fullPath)) { m_processingThread.Push(1, () => TouchAndSkip(fullPath, lastWriteTime)); return; } m_processingThread.Push(2, () => { m_touchedFiles.Remove(fullPath); TouchAndProcess(fullPath, lastWriteTime, 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(() => { DateTime lastWriteTime = File.GetLastWriteTimeUtc(fullPath); 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 (matchesFilter) TouchAndProcess(fullPath, lastWriteTime, true); else TouchAndSkip(fullPath, lastWriteTime); }); }); } // 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)); } // Triggers the error event for the exception encountered by the file watcher. private void Watcher_Error(object sender, ErrorEventArgs args) { Error?.Invoke(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); } } } } #endregion } }