//******************************************************************************************************
// 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
}
}