From ebacf831fc09d4b31feebf54edc8f73072141816 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sun, 4 Dec 2022 09:00:14 +0100 Subject: [PATCH 01/92] FileSystemWatcher is injectable --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 8 ++++-- .../Helpers/FileSystemWatcherWrapper.cs | 28 +++++++++++++++++++ Source/FileWatcherEx/Helpers/FileWatcher.cs | 24 ++++++++-------- 3 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 8bc5e02..e418889 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -19,6 +19,7 @@ public class FileSystemWatcherEx : IDisposable private FileWatcher? _watcher; private FileSystemWatcher? _fsw; + private readonly FileSystemWatcherWrapper? _watcherWrapper; // Define the cancellation token. private CancellationTokenSource? _cancelSource; @@ -124,14 +125,15 @@ public string Filter #endregion - /// /// Initialize new instance of /// /// - public FileSystemWatcherEx(string folderPath = "") + /// optional watcher wrapper, used to inject fake implementation + public FileSystemWatcherEx(string folderPath = "", FileSystemWatcherWrapper? watcherWrapper = null) { FolderPath = folderPath; + _watcherWrapper = watcherWrapper; } @@ -261,7 +263,7 @@ void onError(ErrorEventArgs e) // Start watcher _watcher = new FileWatcher(); - _fsw = _watcher.Create(FolderPath, onEvent, onError); + _fsw = _watcher.Create(FolderPath, onEvent, onError, _watcherWrapper); foreach (var filter in Filters) { diff --git a/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs b/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs new file mode 100644 index 0000000..b76eeb8 --- /dev/null +++ b/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs @@ -0,0 +1,28 @@ +namespace FileWatcherEx; + +/// +/// Interface around .NET FileSystemWatcher to be able to replace it with a fake implementation +/// +public interface IFileSystemWatcherWrapper +{ + string Path { get; set; } + bool IncludeSubdirectories { get; set; } + NotifyFilters NotifyFilter { get; set; } + + event FileSystemEventHandler Created; + event FileSystemEventHandler Deleted; + event FileSystemEventHandler Changed; + event RenamedEventHandler Renamed; + event ErrorEventHandler Error; + + int InternalBufferSize { get; set; } +} + +/// +/// Production implementation of IFileSystemWrapper interface. +/// Backed by the existing FileSystemWatcher +/// +public class FileSystemWatcherWrapper : FileSystemWatcher, IFileSystemWatcherWrapper +{ + // empty on purpose +} diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index a632eec..42dc4fe 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -9,7 +9,7 @@ internal class FileWatcher : IDisposable { private string _watchPath = string.Empty; private Action? _eventCallback = null; - private readonly Dictionary _fwDictionary = new(); + private readonly Dictionary _fwDictionary = new(); private Action? _onError = null; @@ -20,20 +20,18 @@ internal class FileWatcher : IDisposable /// onEvent callback /// onError callback /// - public FileSystemWatcher Create(string path, Action onEvent, Action onError) + public FileSystemWatcherWrapper Create(string path, Action onEvent, Action onError, FileSystemWatcherWrapper? watcher = null) { _watchPath = path; _eventCallback = onEvent; _onError = onError; - var watcher = new FileSystemWatcher - { - Path = _watchPath, - IncludeSubdirectories = true, - NotifyFilter = NotifyFilters.LastWrite - | NotifyFilters.FileName - | NotifyFilters.DirectoryName, - }; + watcher ??= new FileSystemWatcherWrapper(); + watcher.Path = _watchPath; + watcher.IncludeSubdirectories = true; + watcher.NotifyFilter = NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName; // Bind internal events to manipulate the possible symbolic links watcher.Created += new(MakeWatcher_Created); @@ -109,7 +107,7 @@ private void MakeWatcher(string path) { if (!_fwDictionary.ContainsKey(path)) { - var fileSystemWatcherRoot = new FileSystemWatcher + var fileSystemWatcherRoot = new FileSystemWatcherWrapper { Path = path, IncludeSubdirectories = true, @@ -139,7 +137,7 @@ private void MakeWatcher(string path) { if (!_fwDictionary.ContainsKey(item.FullName)) { - var fswItem = new FileSystemWatcher + var fswItem = new FileSystemWatcherWrapper { Path = item.FullName, IncludeSubdirectories = true, @@ -173,7 +171,7 @@ private void MakeWatcher_Created(object sender, FileSystemEventArgs e) if (attrs.HasFlag(FileAttributes.Directory) && attrs.HasFlag(FileAttributes.ReparsePoint)) { - var watcherCreated = new FileSystemWatcher + var watcherCreated = new FileSystemWatcherWrapper { Path = e.FullPath, IncludeSubdirectories = true, From addcaf0d44af32b20a50dc20fd63cc21eb35d48e Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 06:35:48 +0100 Subject: [PATCH 02/92] file system events recorder --- .../FileSystemEventRecorder.cs | 127 ++++++++++++++++++ .../FileSystemEventRecorder.csproj | 18 +++ Source/FileWatcherEx.sln | 6 + .../Helpers/FileSystemWatcherWrapper.cs | 3 + Source/FileWatcherEx/Helpers/FileWatcher.cs | 3 +- .../FileSystemEventRecords.cs | 52 +++++++ .../FileWatcherExTests.csproj | 2 +- 7 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 Source/FileSystemEventRecorder/FileSystemEventRecorder.cs create mode 100644 Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj create mode 100644 Source/FileWatcherExTests/FileSystemEventRecords.cs diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs new file mode 100644 index 0000000..7079f45 --- /dev/null +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs @@ -0,0 +1,127 @@ +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Globalization; +using CsvHelper; +using FileWatcherEx; + +namespace FileSystemEventRecorder; + +// event received from C# FileSystemWatcher +internal record EventRecord(string? FileName, string EventName, long NowInTicks); + +// post processed. Calculated before closing program +internal record EventRecordWithDiff( + string? FileName, + string EventName, + long DiffInTicks, // ticks between passed by from the previous event to now + double DiffInMilliseconds // milliseconds between previous event and now. +); + +public static class FileSystemEventRecords +{ + private static readonly ConcurrentQueue EventRecords = new(); + + public static void Main(string[] args) + { + var (watchedDirectory, csvOutputFile) = ProcessArguments(args); + + var watcher = new FileSystemWatcherWrapper(); + watcher.Path = watchedDirectory; + watcher.IncludeSubdirectories = true; + watcher.NotifyFilter = NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName; + + watcher.Created += (_, fileSystemEventArgs) => + EventRecords.Enqueue(new EventRecord(fileSystemEventArgs.Name, "created", Stopwatch.GetTimestamp())); + watcher.Deleted += (_, fileSystemEventArgs) => + EventRecords.Enqueue(new EventRecord(fileSystemEventArgs.Name, "deleted", Stopwatch.GetTimestamp())); + + watcher.Changed += (_, fileSystemEventArgs) => + EventRecords.Enqueue(new EventRecord(fileSystemEventArgs.Name, "changed", Stopwatch.GetTimestamp())); + watcher.Renamed += (_, renamedEventArgs) => + EventRecords.Enqueue(new EventRecord(renamedEventArgs.Name, "rename", Stopwatch.GetTimestamp())); + watcher.Error += (_, errorEventArgs) => + { + EventRecords.Enqueue(new EventRecord(null, "error", Stopwatch.GetTimestamp())); + Console.WriteLine($"Error: {errorEventArgs.GetException()}"); + }; + + // taken from existing code + watcher.InternalBufferSize = 32768; + watcher.EnableRaisingEvents = true; + + Console.WriteLine("Sleeping..."); + Console.CancelKeyPress += (_, _) => + { + Console.WriteLine("Exiting."); + ProcessQueueAndWriteToDisk(csvOutputFile); + Environment.Exit(0); + }; + + while (true) + { + Thread.Sleep(200); + } + } + + private static (string, string) ProcessArguments(string[] args) + { + if (args.Length < 2) + { + Console.WriteLine("Usage: dotnet run [directory to be watched] [output csv file]"); + Environment.Exit(1); + } + + return (args[0], args[1]); + } + + private static void ProcessQueueAndWriteToDisk(string csvOutputFile) + { + if (EventRecords.IsEmpty) + { + Console.WriteLine("Detected no file system events. Nothing is written."); + } + else + { + Console.WriteLine($"Recorded {EventRecords.Count} file system events."); + var records = MapToDiffTicks(); + + Console.WriteLine($"Writing CSV to {csvOutputFile}."); + using (var writer = new StreamWriter(csvOutputFile)) + using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture)) + { + csv.WriteRecords(records); + } + + Console.WriteLine("Done."); + } + } + + // post-process queue. Calculate difference between previous and current event + private static IEnumerable MapToDiffTicks() + { + List eventsWithDiffs = new(); + long previousTicks = 0; + foreach (var eventRecord in EventRecords) + { + var diff = previousTicks switch + { + 0 => 0, // first run + _ => eventRecord.NowInTicks - previousTicks + }; + + previousTicks = eventRecord.NowInTicks; + double diffInMilliseconds = Convert.ToInt64(new TimeSpan(diff).TotalMilliseconds); + + var record = new EventRecordWithDiff( + eventRecord.FileName, + eventRecord.EventName, + diff, + diffInMilliseconds); + eventsWithDiffs.Add(record); + } + + return eventsWithDiffs; + } +} \ No newline at end of file diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj b/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj new file mode 100644 index 0000000..8ccbdba --- /dev/null +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj @@ -0,0 +1,18 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + + diff --git a/Source/FileWatcherEx.sln b/Source/FileWatcherEx.sln index 42f195a..abd9b7d 100644 --- a/Source/FileWatcherEx.sln +++ b/Source/FileWatcherEx.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo.WinForms", "Demo\Demo. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileWatcherExTests", "FileWatcherExTests\FileWatcherExTests.csproj", "{1C0CA67C-369E-4258-B661-2C545B50A6FF}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystemEventRecorder", "FileSystemEventRecorder\FileSystemEventRecorder.csproj", "{F87993D7-2487-41BD-9044-EBEB54BAD13C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Debug|Any CPU.Build.0 = Debug|Any CPU {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Release|Any CPU.Build.0 = Release|Any CPU + {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs b/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs index b76eeb8..f52384e 100644 --- a/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs +++ b/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs @@ -7,6 +7,7 @@ public interface IFileSystemWatcherWrapper { string Path { get; set; } bool IncludeSubdirectories { get; set; } + bool EnableRaisingEvents { get; set; } NotifyFilters NotifyFilter { get; set; } event FileSystemEventHandler Created; @@ -25,4 +26,6 @@ public interface IFileSystemWatcherWrapper public class FileSystemWatcherWrapper : FileSystemWatcher, IFileSystemWatcherWrapper { // empty on purpose + } + diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 42dc4fe..3af5607 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -14,7 +14,7 @@ internal class FileWatcher : IDisposable /// - /// Create new instance of FileSystemWatcher + /// Create new instance of FileSystemWatcherWrapper /// /// Full folder path to watcher /// onEvent callback @@ -47,6 +47,7 @@ public FileSystemWatcherWrapper Create(string path, Action onE watcher.InternalBufferSize = 32768; _fwDictionary.Add(path, watcher); + // this handles sub directories. Probably needs cleanup foreach (var dirInfo in new DirectoryInfo(path).GetDirectories()) { var attrs = File.GetAttributes(dirInfo.FullName); diff --git a/Source/FileWatcherExTests/FileSystemEventRecords.cs b/Source/FileWatcherExTests/FileSystemEventRecords.cs new file mode 100644 index 0000000..8af4c03 --- /dev/null +++ b/Source/FileWatcherExTests/FileSystemEventRecords.cs @@ -0,0 +1,52 @@ +using System.Collections.Concurrent; +using FileWatcherEx; + +namespace FileWatcherExTests; + +public class FileSystemEventRecords +{ + private static ConcurrentQueue queue = new(); + + public static void Main() + { + var watcher = new FileSystemWatcherWrapper(); + watcher.Path = @"c:\temp\fwtest"; // TODO via CLI args + watcher.IncludeSubdirectories = true; + watcher.NotifyFilter = NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName; + + // Bind internal events to manipulate the possible symbolic links + watcher.Created += OnCreated; + watcher.Deleted += OnDeleted; + + watcher.Changed += OnChanged; + watcher.Renamed += OnRenamed; + watcher.Error += OnError; + + //changing this to a higher value can lead into issues when watching UNC drives + watcher.InternalBufferSize = 32768; + } + + private static void OnCreated(object sender, FileSystemEventArgs fileSystemEventArgs) + { + Console.WriteLine(fileSystemEventArgs); + } + + private static void OnDeleted(object sender, FileSystemEventArgs fileSystemEventArgs) + { + + } + private static void OnChanged(object sender, FileSystemEventArgs fileSystemEventArgs) + { + + } + private static void OnRenamed(object sender, RenamedEventArgs renamedEventArgs) + { + + } + private static void OnError(object sender, ErrorEventArgs errorEventArgs) + { + + } +} \ No newline at end of file diff --git a/Source/FileWatcherExTests/FileWatcherExTests.csproj b/Source/FileWatcherExTests/FileWatcherExTests.csproj index 43d48e8..c19b447 100644 --- a/Source/FileWatcherExTests/FileWatcherExTests.csproj +++ b/Source/FileWatcherExTests/FileWatcherExTests.csproj @@ -4,8 +4,8 @@ net6.0 enable enable - false + false From f65e379fc9f427108fe91e02e1925a90f9df5cda Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 06:43:06 +0100 Subject: [PATCH 03/92] README --- Source/FileSystemEventRecorder/README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Source/FileSystemEventRecorder/README.md diff --git a/Source/FileSystemEventRecorder/README.md b/Source/FileSystemEventRecorder/README.md new file mode 100644 index 0000000..da390fb --- /dev/null +++ b/Source/FileSystemEventRecorder/README.md @@ -0,0 +1,18 @@ +# File System Event Recorder + +Command line tool to capture the raw events of [FileSystemWatcher](https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher) +in a CSV file. The CSV file can than be used to write integration tests against *FileWatcherEx*. + +Usage: +````sh +dotnet run C:\temp\fwtest\ C:\temp\fwevents.csv +```` + +Example output: +````csv +FileName,EventName,DiffInTicks,DiffInMilliseconds +foo.txt,created,0,0 +foo.txt,changed,9534,1 +foo.txt,changed,41180869,4118 +foo.txt,deleted,40944961,4094 +```` \ No newline at end of file From 95cb0ea16618b7cf39293ca8e4b4d9d29528999e Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 06:59:53 +0100 Subject: [PATCH 04/92] simple scenarios --- Source/FileWatcherExTests/scenario/create_and_remove_file.csv | 3 +++ .../scenario/create_and_remove_file_wsl2.csv | 4 ++++ Source/FileWatcherExTests/scenario/create_file.csv | 2 ++ 3 files changed, 9 insertions(+) create mode 100644 Source/FileWatcherExTests/scenario/create_and_remove_file.csv create mode 100644 Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv create mode 100644 Source/FileWatcherExTests/scenario/create_file.csv diff --git a/Source/FileWatcherExTests/scenario/create_and_remove_file.csv b/Source/FileWatcherExTests/scenario/create_and_remove_file.csv new file mode 100644 index 0000000..832ac41 --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_and_remove_file.csv @@ -0,0 +1,3 @@ +FileName,EventName,DiffInTicks,DiffInMilliseconds +a.txt,created,0,0 +a.txt,deleted,8931158,893 diff --git a/Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv b/Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv new file mode 100644 index 0000000..d7f4270 --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv @@ -0,0 +1,4 @@ +FileName,EventName,DiffInTicks,DiffInMilliseconds +a.txt,created,0,0 +a.txt,changed,7124,1 +a.txt,deleted,223819,22 diff --git a/Source/FileWatcherExTests/scenario/create_file.csv b/Source/FileWatcherExTests/scenario/create_file.csv new file mode 100644 index 0000000..d26b3da --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_file.csv @@ -0,0 +1,2 @@ +FileName,EventName,DiffInTicks,DiffInMilliseconds +a.txt,created,0,0 From 63fb7aa65889b013215cc1ea2ee24846a8fc1b5d Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 07:19:40 +0100 Subject: [PATCH 05/92] add OldFullPath to recorder --- .../FileSystemEventRecorder.cs | 35 +++++++++++-------- Source/FileSystemEventRecorder/README.md | 9 +++-- Source/FileWatcherExTests/scenario/README.md | 26 ++++++++++++++ .../scenario/create_and_remove_file.csv | 6 ++-- .../scenario/create_and_remove_file_wsl2.csv | 8 ++--- .../scenario/create_file.csv | 4 +-- .../create_rename_and_remove_file.csv | 4 +++ 7 files changed, 64 insertions(+), 28 deletions(-) create mode 100644 Source/FileWatcherExTests/scenario/README.md create mode 100644 Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs index 7079f45..418b01f 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs @@ -7,12 +7,18 @@ namespace FileSystemEventRecorder; // event received from C# FileSystemWatcher -internal record EventRecord(string? FileName, string EventName, long NowInTicks); +internal record EventRecord( + string FullPath, + string EventName, + string? OldFullPath, // only provided by "rename" event + long NowInTicks +); // post processed. Calculated before closing program internal record EventRecordWithDiff( - string? FileName, + string FullPath, string EventName, + string? OldFullPath, long DiffInTicks, // ticks between passed by from the previous event to now double DiffInMilliseconds // milliseconds between previous event and now. ); @@ -32,19 +38,19 @@ public static void Main(string[] args) | NotifyFilters.FileName | NotifyFilters.DirectoryName; - watcher.Created += (_, fileSystemEventArgs) => - EventRecords.Enqueue(new EventRecord(fileSystemEventArgs.Name, "created", Stopwatch.GetTimestamp())); - watcher.Deleted += (_, fileSystemEventArgs) => - EventRecords.Enqueue(new EventRecord(fileSystemEventArgs.Name, "deleted", Stopwatch.GetTimestamp())); + watcher.Created += (_, ev) => + EventRecords.Enqueue(new EventRecord(ev.FullPath, "created", null, Stopwatch.GetTimestamp())); + watcher.Deleted += (_, ev) => + EventRecords.Enqueue(new EventRecord(ev.FullPath, "deleted", null, Stopwatch.GetTimestamp())); - watcher.Changed += (_, fileSystemEventArgs) => - EventRecords.Enqueue(new EventRecord(fileSystemEventArgs.Name, "changed", Stopwatch.GetTimestamp())); - watcher.Renamed += (_, renamedEventArgs) => - EventRecords.Enqueue(new EventRecord(renamedEventArgs.Name, "rename", Stopwatch.GetTimestamp())); - watcher.Error += (_, errorEventArgs) => + watcher.Changed += (_, ev) => + EventRecords.Enqueue(new EventRecord(ev.FullPath, "changed", null, Stopwatch.GetTimestamp())); + watcher.Renamed += (_, ev) => + EventRecords.Enqueue(new EventRecord(ev.FullPath, "rename", ev.OldFullPath, Stopwatch.GetTimestamp())); + watcher.Error += (_, ev) => { - EventRecords.Enqueue(new EventRecord(null, "error", Stopwatch.GetTimestamp())); - Console.WriteLine($"Error: {errorEventArgs.GetException()}"); + EventRecords.Enqueue(new EventRecord("", "error", null, Stopwatch.GetTimestamp())); + Console.WriteLine($"Error: {ev.GetException()}"); }; // taken from existing code @@ -115,8 +121,9 @@ private static IEnumerable MapToDiffTicks() double diffInMilliseconds = Convert.ToInt64(new TimeSpan(diff).TotalMilliseconds); var record = new EventRecordWithDiff( - eventRecord.FileName, + eventRecord.FullPath, eventRecord.EventName, + eventRecord.OldFullPath, diff, diffInMilliseconds); eventsWithDiffs.Add(record); diff --git a/Source/FileSystemEventRecorder/README.md b/Source/FileSystemEventRecorder/README.md index da390fb..efd42f2 100644 --- a/Source/FileSystemEventRecorder/README.md +++ b/Source/FileSystemEventRecorder/README.md @@ -10,9 +10,8 @@ dotnet run C:\temp\fwtest\ C:\temp\fwevents.csv Example output: ````csv -FileName,EventName,DiffInTicks,DiffInMilliseconds -foo.txt,created,0,0 -foo.txt,changed,9534,1 -foo.txt,changed,41180869,4118 -foo.txt,deleted,40944961,4094 +FullPath,EventName,OldFullPath,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest\a.txt,created,,0,0 +C:\temp\fwtest\b.txt,rename,C:\temp\fwtest\a.txt,425188,43 +C:\temp\fwtest\b.txt,deleted,,6695305,670 ```` \ No newline at end of file diff --git a/Source/FileWatcherExTests/scenario/README.md b/Source/FileWatcherExTests/scenario/README.md new file mode 100644 index 0000000..d57d00d --- /dev/null +++ b/Source/FileWatcherExTests/scenario/README.md @@ -0,0 +1,26 @@ +## `create_file.csv` +````powershell +New-Item -Path 'c:\temp\fwtest\a.txt' -ItemType File +```` + +## `create_and_remove_file.csv` +````powershell +New-Item -Path 'c:\temp\fwtest\a.txt' -ItemType File +Remove-Item -Path 'c:\temp\fwtest\a.txt' -Recurse +```` + +## `create_rename_and_remove_file.csv` +````powershell +New-Item -Path 'c:\temp\fwtest\a.txt' -ItemType File +Rename-Item -Path 'c:\temp\fwtest\a.txt' -NewName 'c:\temp\fwtest\b.txt' +Remove-Item -Path 'c:\temp\fwtest\b.txt' -Recurse +```` + +## `create_and_remove_file_wsl2.csv` +Create and remove file in WSL 2. On file creation, a second "changed" event is written. +````sh +touch /mnt/c/temp/fwtest/a.txt +rm /mnt/c/temp/fwtest/a.txt +```` + + diff --git a/Source/FileWatcherExTests/scenario/create_and_remove_file.csv b/Source/FileWatcherExTests/scenario/create_and_remove_file.csv index 832ac41..dc18ae7 100644 --- a/Source/FileWatcherExTests/scenario/create_and_remove_file.csv +++ b/Source/FileWatcherExTests/scenario/create_and_remove_file.csv @@ -1,3 +1,3 @@ -FileName,EventName,DiffInTicks,DiffInMilliseconds -a.txt,created,0,0 -a.txt,deleted,8931158,893 +FullPath,EventName,OldFullPath,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest\a.txt,created,,0,0 +C:\temp\fwtest\a.txt,deleted,,6170905,617 diff --git a/Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv b/Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv index d7f4270..d0bd7bf 100644 --- a/Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv +++ b/Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv @@ -1,4 +1,4 @@ -FileName,EventName,DiffInTicks,DiffInMilliseconds -a.txt,created,0,0 -a.txt,changed,7124,1 -a.txt,deleted,223819,22 +FullPath,EventName,OldFullPath,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest\a.txt,created,,0,0 +C:\temp\fwtest\a.txt,changed,,7204,1 +C:\temp\fwtest\a.txt,deleted,,165845,17 diff --git a/Source/FileWatcherExTests/scenario/create_file.csv b/Source/FileWatcherExTests/scenario/create_file.csv index d26b3da..38dfb85 100644 --- a/Source/FileWatcherExTests/scenario/create_file.csv +++ b/Source/FileWatcherExTests/scenario/create_file.csv @@ -1,2 +1,2 @@ -FileName,EventName,DiffInTicks,DiffInMilliseconds -a.txt,created,0,0 +FullPath,EventName,OldFullPath,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest\a.txt,created,,0,0 diff --git a/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv b/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv new file mode 100644 index 0000000..84461fc --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv @@ -0,0 +1,4 @@ +FullPath,EventName,OldFullPath,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest\a.txt,created,,0,0 +C:\temp\fwtest\b.txt,rename,C:\temp\fwtest\a.txt,425188,43 +C:\temp\fwtest\b.txt,deleted,,6695305,670 From 1244ddc74256f99b79ac8defbf8f3e0d6d2f18c8 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 07:21:36 +0100 Subject: [PATCH 06/92] cleanup --- .../FileSystemEventRecords.cs | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 Source/FileWatcherExTests/FileSystemEventRecords.cs diff --git a/Source/FileWatcherExTests/FileSystemEventRecords.cs b/Source/FileWatcherExTests/FileSystemEventRecords.cs deleted file mode 100644 index 8af4c03..0000000 --- a/Source/FileWatcherExTests/FileSystemEventRecords.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Collections.Concurrent; -using FileWatcherEx; - -namespace FileWatcherExTests; - -public class FileSystemEventRecords -{ - private static ConcurrentQueue queue = new(); - - public static void Main() - { - var watcher = new FileSystemWatcherWrapper(); - watcher.Path = @"c:\temp\fwtest"; // TODO via CLI args - watcher.IncludeSubdirectories = true; - watcher.NotifyFilter = NotifyFilters.LastWrite - | NotifyFilters.FileName - | NotifyFilters.DirectoryName; - - // Bind internal events to manipulate the possible symbolic links - watcher.Created += OnCreated; - watcher.Deleted += OnDeleted; - - watcher.Changed += OnChanged; - watcher.Renamed += OnRenamed; - watcher.Error += OnError; - - //changing this to a higher value can lead into issues when watching UNC drives - watcher.InternalBufferSize = 32768; - } - - private static void OnCreated(object sender, FileSystemEventArgs fileSystemEventArgs) - { - Console.WriteLine(fileSystemEventArgs); - } - - private static void OnDeleted(object sender, FileSystemEventArgs fileSystemEventArgs) - { - - } - private static void OnChanged(object sender, FileSystemEventArgs fileSystemEventArgs) - { - - } - private static void OnRenamed(object sender, RenamedEventArgs renamedEventArgs) - { - - } - private static void OnError(object sender, ErrorEventArgs errorEventArgs) - { - - } -} \ No newline at end of file From b4669fd04574a59676ecbe77cb81140e6830c928 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 07:48:35 +0100 Subject: [PATCH 07/92] simple real file system test --- .../FileWatcherExIntegrationTest.cs | 39 +++++++++++++++++++ .../FileWatcherExTests.csproj | 1 - 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs new file mode 100644 index 0000000..ec861e2 --- /dev/null +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -0,0 +1,39 @@ +using System.Collections.Concurrent; +using FileWatcherEx; +using Xunit; +using Xunit.Abstractions; + +namespace FileWatcherExTests; + +public class FileWatcherExIntegrationTest +{ + [Fact] + public void SimpleRealFileSystemTest() + { + ConcurrentQueue events = new(); + var fw = new FileSystemWatcherEx(@"c:\temp\fwtest\"); + + fw.OnCreated += (_, ev) => events.Enqueue(ev); + fw.OnDeleted += (_, ev) => events.Enqueue(ev); + fw.OnChanged += (_, ev) => events.Enqueue(ev); + fw.OnRenamed += (_, ev) => events.Enqueue(ev); + fw.OnRenamed += (_, ev) => events.Enqueue(ev); + + const string testFile = @"c:\temp\fwtest\b.txt"; + if (File.Exists(testFile)) + { + File.Delete(testFile); + } + + fw.Start(); + File.Create(testFile); + Thread.Sleep(250); + fw.Stop(); + + Assert.Single(events); + var ev = events.First(); + Assert.Equal(ChangeType.CREATED, ev.ChangeType); + Assert.Equal(@"c:\temp\fwtest\b.txt", ev.FullPath); + Assert.Equal("", ev.OldFullPath); + } +} \ No newline at end of file diff --git a/Source/FileWatcherExTests/FileWatcherExTests.csproj b/Source/FileWatcherExTests/FileWatcherExTests.csproj index c19b447..49b6527 100644 --- a/Source/FileWatcherExTests/FileWatcherExTests.csproj +++ b/Source/FileWatcherExTests/FileWatcherExTests.csproj @@ -5,7 +5,6 @@ enable enable false - false From 0e78b4a11791037458784bb5bf3c0fc64f246127 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 08:47:25 +0100 Subject: [PATCH 08/92] fix Wrapper injection --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 6 +++--- .../Helpers/FileSystemWatcherWrapper.cs | 14 +++++++++++--- Source/FileWatcherEx/Helpers/FileWatcher.cs | 5 +++-- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index e418889..b35225f 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -18,8 +18,8 @@ public class FileSystemWatcherEx : IDisposable private readonly BlockingCollection _fileEventQueue = new(); private FileWatcher? _watcher; - private FileSystemWatcher? _fsw; - private readonly FileSystemWatcherWrapper? _watcherWrapper; + private IFileSystemWatcherWrapper? _fsw; + private readonly IFileSystemWatcherWrapper? _watcherWrapper; // Define the cancellation token. private CancellationTokenSource? _cancelSource; @@ -130,7 +130,7 @@ public string Filter /// /// /// optional watcher wrapper, used to inject fake implementation - public FileSystemWatcherEx(string folderPath = "", FileSystemWatcherWrapper? watcherWrapper = null) + public FileSystemWatcherEx(string folderPath = "", IFileSystemWatcherWrapper? watcherWrapper = null) { FolderPath = folderPath; _watcherWrapper = watcherWrapper; diff --git a/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs b/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs index f52384e..05c4438 100644 --- a/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs +++ b/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs @@ -1,4 +1,7 @@ -namespace FileWatcherEx; +using System.Collections.ObjectModel; +using System.ComponentModel; + +namespace FileWatcherEx; /// /// Interface around .NET FileSystemWatcher to be able to replace it with a fake implementation @@ -6,6 +9,8 @@ public interface IFileSystemWatcherWrapper { string Path { get; set; } + + Collection Filters { get; } bool IncludeSubdirectories { get; set; } bool EnableRaisingEvents { get; set; } NotifyFilters NotifyFilter { get; set; } @@ -17,6 +22,10 @@ public interface IFileSystemWatcherWrapper event ErrorEventHandler Error; int InternalBufferSize { get; set; } + + public ISynchronizeInvoke? SynchronizingObject { get; set; } + + void Dispose(); } /// @@ -25,7 +34,6 @@ public interface IFileSystemWatcherWrapper /// public class FileSystemWatcherWrapper : FileSystemWatcher, IFileSystemWatcherWrapper { - // empty on purpose - + // intentionally empty } diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 3af5607..0ebedfb 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -9,7 +9,7 @@ internal class FileWatcher : IDisposable { private string _watchPath = string.Empty; private Action? _eventCallback = null; - private readonly Dictionary _fwDictionary = new(); + private readonly Dictionary _fwDictionary = new(); private Action? _onError = null; @@ -19,8 +19,9 @@ internal class FileWatcher : IDisposable /// Full folder path to watcher /// onEvent callback /// onError callback + /// /// - public FileSystemWatcherWrapper Create(string path, Action onEvent, Action onError, FileSystemWatcherWrapper? watcher = null) + public IFileSystemWatcherWrapper Create(string path, Action onEvent, Action onError, IFileSystemWatcherWrapper? watcher = null) { _watchPath = path; _eventCallback = onEvent; From 5dcb56b977e0bb7566d43c80af1080219cba9df2 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 09:25:41 +0100 Subject: [PATCH 09/92] first replayer test --- .../FileSystemEventRecorder.cs | 2 +- .../FileWatcherExIntegrationTest.cs | 106 ++++++++++++++++++ .../FileWatcherExTests.csproj | 7 ++ 3 files changed, 114 insertions(+), 1 deletion(-) diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs index 418b01f..0b0385d 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs @@ -46,7 +46,7 @@ public static void Main(string[] args) watcher.Changed += (_, ev) => EventRecords.Enqueue(new EventRecord(ev.FullPath, "changed", null, Stopwatch.GetTimestamp())); watcher.Renamed += (_, ev) => - EventRecords.Enqueue(new EventRecord(ev.FullPath, "rename", ev.OldFullPath, Stopwatch.GetTimestamp())); + EventRecords.Enqueue(new EventRecord(ev.FullPath, "renamed", ev.OldFullPath, Stopwatch.GetTimestamp())); watcher.Error += (_, ev) => { EventRecords.Enqueue(new EventRecord("", "error", null, Stopwatch.GetTimestamp())); diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index ec861e2..fc62ae7 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -1,12 +1,118 @@ using System.Collections.Concurrent; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using CsvHelper; using FileWatcherEx; using Xunit; using Xunit.Abstractions; namespace FileWatcherExTests; +internal record EventRecordWithDiff( + string FullPath, + string EventName, + string? OldFullPath, + long DiffInTicks, + double DiffInMilliseconds +); + +class ReplayFileSystemWatcherWrapper : IFileSystemWatcherWrapper +{ + private string _csvFile; + public ReplayFileSystemWatcherWrapper(string csvFile) + { + _csvFile = csvFile; + } + + public string Path { get; set; } + public Collection Filters { get; } + public bool IncludeSubdirectories { get; set; } + public bool EnableRaisingEvents { get; set; } + public NotifyFilters NotifyFilter { get; set; } + public event FileSystemEventHandler? Created; + public event FileSystemEventHandler? Deleted; + public event FileSystemEventHandler? Changed; + public event RenamedEventHandler? Renamed; + public event ErrorEventHandler? Error; + public int InternalBufferSize { get; set; } + public ISynchronizeInvoke? SynchronizingObject { get; set; } + public void Dispose() + { + } + + public void Replay() + { + using var reader = new StreamReader(_csvFile); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + + var records = csv.GetRecords(); + foreach (var record in records) + { + var directory = System.IO.Path.GetDirectoryName(record.FullPath); + var fileName = System.IO.Path.GetFileName(record.FullPath); + + switch (record.EventName) + { + case "created": + { + var ev = new FileSystemEventArgs(WatcherChangeTypes.Created, directory, fileName); + Created?.Invoke(this, ev); + break; + } + case "deleted": + { + var ev = new FileSystemEventArgs(WatcherChangeTypes.Deleted, directory, fileName); + Deleted?.Invoke(this, ev); + break; + } + case "changed": + { + var ev = new FileSystemEventArgs(WatcherChangeTypes.Changed, directory, fileName); + Changed?.Invoke(this, ev); + break; + } + case "renamed": + { + var oldFileName = System.IO.Path.GetFileName(record.OldFullPath); + var ev = new RenamedEventArgs(WatcherChangeTypes.Renamed, directory, fileName, oldFileName); + Renamed?.Invoke(this, ev); + break; + } + } + } + } +} + public class FileWatcherExIntegrationTest { + + [Fact] + public void Foo() + { + ConcurrentQueue events = new(); + var replayFw = new ReplayFileSystemWatcherWrapper(@"scenario\create_file.csv"); + var unusedDir = Path.GetTempPath(); + var fw = new FileSystemWatcherEx(unusedDir, replayFw); + + fw.OnCreated += (_, ev) => events.Enqueue(ev); + fw.OnDeleted += (_, ev) => events.Enqueue(ev); + fw.OnChanged += (_, ev) => events.Enqueue(ev); + fw.OnRenamed += (_, ev) => events.Enqueue(ev); + + fw.Start(); + replayFw.Replay(); + Thread.Sleep(250); + fw.Stop(); + + Assert.Single(events); + var ev = events.First(); + Assert.Equal(ChangeType.CREATED, ev.ChangeType); + Assert.Equal(@"C:\temp\fwtest\a.txt", ev.FullPath); + Assert.Equal("", ev.OldFullPath); + } + + [Fact] public void SimpleRealFileSystemTest() { diff --git a/Source/FileWatcherExTests/FileWatcherExTests.csproj b/Source/FileWatcherExTests/FileWatcherExTests.csproj index 49b6527..7d07fd8 100644 --- a/Source/FileWatcherExTests/FileWatcherExTests.csproj +++ b/Source/FileWatcherExTests/FileWatcherExTests.csproj @@ -8,6 +8,7 @@ + @@ -23,5 +24,11 @@ + + + + PreserveNewest + + From f61a1a79d1ef473a5be3d3b0f86beedff8082c0c Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 09:57:23 +0100 Subject: [PATCH 10/92] refactor --- .../FileSystemEventRecorder.cs | 17 +++- Source/FileSystemEventRecorder/README.md | 8 +- .../FileWatcherExIntegrationTest.cs | 96 ++----------------- .../ReplayFileSystemWatcherWrapper.cs | 86 +++++++++++++++++ .../scenario/create_file.csv | 4 +- .../create_rename_and_remove_file.csv | 8 +- 6 files changed, 118 insertions(+), 101 deletions(-) create mode 100644 Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs index 0b0385d..b85c515 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs @@ -14,11 +14,13 @@ internal record EventRecord( long NowInTicks ); -// post processed. Calculated before closing program +// post processed. Calculated before closing program. +// data is written to CSV already in the format FileSystemEventArgs requires it (separate dir + filename) internal record EventRecordWithDiff( - string FullPath, + string Directory, + string FileName, string EventName, - string? OldFullPath, + string? OldFileName, long DiffInTicks, // ticks between passed by from the previous event to now double DiffInMilliseconds // milliseconds between previous event and now. ); @@ -120,10 +122,15 @@ private static IEnumerable MapToDiffTicks() previousTicks = eventRecord.NowInTicks; double diffInMilliseconds = Convert.ToInt64(new TimeSpan(diff).TotalMilliseconds); + var directory = Path.GetDirectoryName(eventRecord.FullPath) ?? ""; + var fileName = Path.GetFileName(eventRecord.FullPath); + var oldFileName = Path.GetFileName(eventRecord.OldFullPath); + var record = new EventRecordWithDiff( - eventRecord.FullPath, + directory, + fileName, eventRecord.EventName, - eventRecord.OldFullPath, + oldFileName, diff, diffInMilliseconds); eventsWithDiffs.Add(record); diff --git a/Source/FileSystemEventRecorder/README.md b/Source/FileSystemEventRecorder/README.md index efd42f2..80b2f40 100644 --- a/Source/FileSystemEventRecorder/README.md +++ b/Source/FileSystemEventRecorder/README.md @@ -10,8 +10,8 @@ dotnet run C:\temp\fwtest\ C:\temp\fwevents.csv Example output: ````csv -FullPath,EventName,OldFullPath,DiffInTicks,DiffInMilliseconds -C:\temp\fwtest\a.txt,created,,0,0 -C:\temp\fwtest\b.txt,rename,C:\temp\fwtest\a.txt,425188,43 -C:\temp\fwtest\b.txt,deleted,,6695305,670 +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,a.txt,created,,0,0 +C:\temp\fwtest,b.txt,renamed,a.txt,1265338,127 +C:\temp\fwtest,b.txt,deleted,,6660690,666 ```` \ No newline at end of file diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index fc62ae7..21c85c9 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -9,101 +9,25 @@ namespace FileWatcherExTests; -internal record EventRecordWithDiff( - string FullPath, - string EventName, - string? OldFullPath, - long DiffInTicks, - double DiffInMilliseconds -); - -class ReplayFileSystemWatcherWrapper : IFileSystemWatcherWrapper -{ - private string _csvFile; - public ReplayFileSystemWatcherWrapper(string csvFile) - { - _csvFile = csvFile; - } - - public string Path { get; set; } - public Collection Filters { get; } - public bool IncludeSubdirectories { get; set; } - public bool EnableRaisingEvents { get; set; } - public NotifyFilters NotifyFilter { get; set; } - public event FileSystemEventHandler? Created; - public event FileSystemEventHandler? Deleted; - public event FileSystemEventHandler? Changed; - public event RenamedEventHandler? Renamed; - public event ErrorEventHandler? Error; - public int InternalBufferSize { get; set; } - public ISynchronizeInvoke? SynchronizingObject { get; set; } - public void Dispose() - { - } - - public void Replay() - { - using var reader = new StreamReader(_csvFile); - using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); - - var records = csv.GetRecords(); - foreach (var record in records) - { - var directory = System.IO.Path.GetDirectoryName(record.FullPath); - var fileName = System.IO.Path.GetFileName(record.FullPath); - - switch (record.EventName) - { - case "created": - { - var ev = new FileSystemEventArgs(WatcherChangeTypes.Created, directory, fileName); - Created?.Invoke(this, ev); - break; - } - case "deleted": - { - var ev = new FileSystemEventArgs(WatcherChangeTypes.Deleted, directory, fileName); - Deleted?.Invoke(this, ev); - break; - } - case "changed": - { - var ev = new FileSystemEventArgs(WatcherChangeTypes.Changed, directory, fileName); - Changed?.Invoke(this, ev); - break; - } - case "renamed": - { - var oldFileName = System.IO.Path.GetFileName(record.OldFullPath); - var ev = new RenamedEventArgs(WatcherChangeTypes.Renamed, directory, fileName, oldFileName); - Renamed?.Invoke(this, ev); - break; - } - } - } - } -} - public class FileWatcherExIntegrationTest { [Fact] - public void Foo() + public void Create_Single_File() { ConcurrentQueue events = new(); - var replayFw = new ReplayFileSystemWatcherWrapper(@"scenario\create_file.csv"); + ReplayFileSystemWatcherWrapper replayer = new(); var unusedDir = Path.GetTempPath(); - var fw = new FileSystemWatcherEx(unusedDir, replayFw); + var fileWatcher = new FileSystemWatcherEx(unusedDir, replayer); - fw.OnCreated += (_, ev) => events.Enqueue(ev); - fw.OnDeleted += (_, ev) => events.Enqueue(ev); - fw.OnChanged += (_, ev) => events.Enqueue(ev); - fw.OnRenamed += (_, ev) => events.Enqueue(ev); + fileWatcher.OnCreated += (_, ev) => events.Enqueue(ev); + fileWatcher.OnDeleted += (_, ev) => events.Enqueue(ev); + fileWatcher.OnChanged += (_, ev) => events.Enqueue(ev); + fileWatcher.OnRenamed += (_, ev) => events.Enqueue(ev); - fw.Start(); - replayFw.Replay(); - Thread.Sleep(250); - fw.Stop(); + fileWatcher.Start(); + replayer.Replay(@"scenario\create_file.csv"); + fileWatcher.Stop(); Assert.Single(events); var ev = events.First(); diff --git a/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs b/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs new file mode 100644 index 0000000..6156b14 --- /dev/null +++ b/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs @@ -0,0 +1,86 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Globalization; +using CsvHelper; +using FileWatcherEx; + +namespace FileWatcherExTests; + +internal record EventRecordWithDiff( + string Directory, + string FileName, + string EventName, + string? OldFileName, + long DiffInTicks, + double DiffInMilliseconds +); + +/// +/// Allows replaying of previously recorded file system events. +/// Used for integration testing. +/// +public class ReplayFileSystemWatcherWrapper : IFileSystemWatcherWrapper +{ + public void Replay(string csvFile) + { + using var reader = new StreamReader(csvFile); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + + var records = csv.GetRecords(); + foreach (var record in records) + { + // introduce time gap like originally recorded + Thread.Sleep((int)record.DiffInMilliseconds); + + switch (record.EventName) + { + case "created": + { + var ev = new FileSystemEventArgs(WatcherChangeTypes.Created, record.Directory, record.FileName); + Created?.Invoke(this, ev); + break; + } + case "deleted": + { + var ev = new FileSystemEventArgs(WatcherChangeTypes.Deleted, record.Directory, record.FileName); + Deleted?.Invoke(this, ev); + break; + } + case "changed": + { + var ev = new FileSystemEventArgs(WatcherChangeTypes.Changed, record.Directory, record.FileName); + Changed?.Invoke(this, ev); + break; + } + case "renamed": + { + var ev = new RenamedEventArgs(WatcherChangeTypes.Renamed, record.Directory, record.FileName, + record.OldFileName); + Renamed?.Invoke(this, ev); + break; + } + } + } + // settle down + Thread.Sleep(250); + } + + public event FileSystemEventHandler? Created; + public event FileSystemEventHandler? Deleted; + public event FileSystemEventHandler? Changed; + public event RenamedEventHandler? Renamed; + + // unused in replay implementation + public string Path { get; set; } + public Collection Filters { get; } + public bool IncludeSubdirectories { get; set; } + public bool EnableRaisingEvents { get; set; } + public NotifyFilters NotifyFilter { get; set; } + public event ErrorEventHandler? Error; + public int InternalBufferSize { get; set; } + public ISynchronizeInvoke? SynchronizingObject { get; set; } + + public void Dispose() + { + } +} \ No newline at end of file diff --git a/Source/FileWatcherExTests/scenario/create_file.csv b/Source/FileWatcherExTests/scenario/create_file.csv index 38dfb85..d5b452c 100644 --- a/Source/FileWatcherExTests/scenario/create_file.csv +++ b/Source/FileWatcherExTests/scenario/create_file.csv @@ -1,2 +1,2 @@ -FullPath,EventName,OldFullPath,DiffInTicks,DiffInMilliseconds -C:\temp\fwtest\a.txt,created,,0,0 +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,a.txt,created,,0,0 diff --git a/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv b/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv index 84461fc..d31825f 100644 --- a/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv +++ b/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv @@ -1,4 +1,4 @@ -FullPath,EventName,OldFullPath,DiffInTicks,DiffInMilliseconds -C:\temp\fwtest\a.txt,created,,0,0 -C:\temp\fwtest\b.txt,rename,C:\temp\fwtest\a.txt,425188,43 -C:\temp\fwtest\b.txt,deleted,,6695305,670 +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,a.txt,created,,0,0 +C:\temp\fwtest,b.txt,renamed,a.txt,1265338,127 +C:\temp\fwtest,b.txt,deleted,,6660690,666 From 17bdb8fd47e40beedaab0fdc7f01c1f40cf9321f Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 10:02:16 +0100 Subject: [PATCH 11/92] refactor --- .../FileWatcherExIntegrationTest.cs | 47 ++++++++++--------- 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 21c85c9..e687f06 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -1,43 +1,46 @@ using System.Collections.Concurrent; -using System.Collections.ObjectModel; -using System.ComponentModel; -using System.Globalization; -using CsvHelper; using FileWatcherEx; using Xunit; -using Xunit.Abstractions; namespace FileWatcherExTests; public class FileWatcherExIntegrationTest { + private ConcurrentQueue _events; + private ReplayFileSystemWatcherWrapper _replayer; + private FileSystemWatcherEx _fileWatcher; + + public FileWatcherExIntegrationTest() + { + // setup before each test run + _events = new(); + _replayer = new(); + var unusedDir = Path.GetTempPath(); + _fileWatcher = new FileSystemWatcherEx(unusedDir, _replayer); + + _fileWatcher.OnCreated += (_, ev) => _events.Enqueue(ev); + _fileWatcher.OnDeleted += (_, ev) => _events.Enqueue(ev); + _fileWatcher.OnChanged += (_, ev) => _events.Enqueue(ev); + _fileWatcher.OnRenamed += (_, ev) => _events.Enqueue(ev); + } + [Fact] public void Create_Single_File() { - ConcurrentQueue events = new(); - ReplayFileSystemWatcherWrapper replayer = new(); - var unusedDir = Path.GetTempPath(); - var fileWatcher = new FileSystemWatcherEx(unusedDir, replayer); + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_file.csv"); + _fileWatcher.Stop(); - fileWatcher.OnCreated += (_, ev) => events.Enqueue(ev); - fileWatcher.OnDeleted += (_, ev) => events.Enqueue(ev); - fileWatcher.OnChanged += (_, ev) => events.Enqueue(ev); - fileWatcher.OnRenamed += (_, ev) => events.Enqueue(ev); - - fileWatcher.Start(); - replayer.Replay(@"scenario\create_file.csv"); - fileWatcher.Stop(); - - Assert.Single(events); - var ev = events.First(); + Assert.Single(_events); + var ev = _events.First(); Assert.Equal(ChangeType.CREATED, ev.ChangeType); Assert.Equal(@"C:\temp\fwtest\a.txt", ev.FullPath); Assert.Equal("", ev.OldFullPath); } - - [Fact] + + [Fact (Skip = "requires real (Windows) file system")] public void SimpleRealFileSystemTest() { ConcurrentQueue events = new(); From 8185cad3dfa938269d423bd1c8a10e7a052793bf Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 11:18:10 +0100 Subject: [PATCH 12/92] more scenarios --- .../FileSystemEventRecorder.cs | 3 +- .../FileWatcherExIntegrationTest.cs | 94 ++++++++++++++++++- Source/FileWatcherExTests/scenario/README.md | 21 ++++- .../scenario/create_and_remove_file.csv | 6 +- .../scenario/create_and_remove_file_wsl2.csv | 4 - .../scenario/create_and_rename_file_wsl2.csv | 4 + .../scenario/create_file_wsl2.csv | 3 + .../create_rename_and_remove_file.csv | 4 +- .../create_rename_and_remove_file_wsl2.csv | 5 + 9 files changed, 127 insertions(+), 17 deletions(-) delete mode 100644 Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv create mode 100644 Source/FileWatcherExTests/scenario/create_and_rename_file_wsl2.csv create mode 100644 Source/FileWatcherExTests/scenario/create_file_wsl2.csv create mode 100644 Source/FileWatcherExTests/scenario/create_rename_and_remove_file_wsl2.csv diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs index b85c515..d969e09 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs @@ -59,7 +59,8 @@ public static void Main(string[] args) watcher.InternalBufferSize = 32768; watcher.EnableRaisingEvents = true; - Console.WriteLine("Sleeping..."); + Console.WriteLine($"Recording. Now go ahead and perform the desired file system activities in {watchedDirectory}. " + + "Press CTRL + C to stop the recording."); Console.CancelKeyPress += (_, _) => { Console.WriteLine("Exiting."); diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index e687f06..6703c84 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -23,8 +23,8 @@ public FileWatcherExIntegrationTest() _fileWatcher.OnChanged += (_, ev) => _events.Enqueue(ev); _fileWatcher.OnRenamed += (_, ev) => _events.Enqueue(ev); } - - + + [Fact] public void Create_Single_File() { @@ -38,9 +38,95 @@ public void Create_Single_File() Assert.Equal(@"C:\temp\fwtest\a.txt", ev.FullPath); Assert.Equal("", ev.OldFullPath); } + + [Fact] + public void Create_And_Remove_Single_File() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_and_remove_file.csv"); + _fileWatcher.Stop(); + + Assert.Equal(2, _events.Count); + var ev1 = _events.ToList()[0]; + var ev2 = _events.ToList()[1]; + + Assert.Equal(ChangeType.CREATED, ev1.ChangeType); + Assert.Equal(@"C:\temp\fwtest\a.txt", ev1.FullPath); + Assert.Equal("", ev1.OldFullPath); + + Assert.Equal(ChangeType.DELETED, ev2.ChangeType); + Assert.Equal(@"C:\temp\fwtest\a.txt", ev2.FullPath); + Assert.Equal("", ev2.OldFullPath); + } + + + [Fact] + public void Create_Rename_And_Remove_Single_File() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_rename_and_remove_file.csv"); + _fileWatcher.Stop(); + + Assert.Equal(3, _events.Count); + var ev1 = _events.ToList()[0]; + var ev2 = _events.ToList()[1]; + var ev3 = _events.ToList()[2]; + + Assert.Equal(ChangeType.CREATED, ev1.ChangeType); + Assert.Equal(@"C:\temp\fwtest\a.txt", ev1.FullPath); + + Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); + Assert.Equal(@"C:\temp\fwtest\b.txt", ev2.FullPath); + Assert.Equal(@"C:\temp\fwtest\a.txt", ev2.OldFullPath); + + Assert.Equal(ChangeType.DELETED, ev3.ChangeType); + Assert.Equal(@"C:\temp\fwtest\b.txt", ev3.FullPath); + Assert.Equal("", ev3.OldFullPath); + } + + [Fact] + // filters out 2nd "changed" event + public void Create_Single_File_Via_WSL2() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_file_wsl2.csv"); + _fileWatcher.Stop(); + + Assert.Single(_events); + var ev = _events.First(); + Assert.Equal(ChangeType.CREATED, ev.ChangeType); + Assert.Equal(@"C:\temp\fwtest\a.txt", ev.FullPath); + Assert.Equal("", ev.OldFullPath); + } + + [Fact] + // scenario creates "created" "changed" and "renamed" event. + // resulting event is just "created" with the filename taken from "renamed" + public void Create_And_Rename_Single_File_Via_WSL2() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_and_rename_file_wsl2.csv"); + _fileWatcher.Stop(); + + Assert.Single(_events); + var ev = _events.First(); + Assert.Equal(ChangeType.CREATED, ev.ChangeType); + Assert.Equal(@"C:\temp\fwtest\b.txt", ev.FullPath); + Assert.Null(ev.OldFullPath); + } + + [Fact] + public void Create_Rename_And_Remove_Single_File_Via_WSL2() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_rename_and_remove_file_wsl2.csv"); + _fileWatcher.Stop(); + + Assert.Empty(_events); + } + - - [Fact (Skip = "requires real (Windows) file system")] + [Fact(Skip = "requires real (Windows) file system")] public void SimpleRealFileSystemTest() { ConcurrentQueue events = new(); diff --git a/Source/FileWatcherExTests/scenario/README.md b/Source/FileWatcherExTests/scenario/README.md index d57d00d..f5e776e 100644 --- a/Source/FileWatcherExTests/scenario/README.md +++ b/Source/FileWatcherExTests/scenario/README.md @@ -16,11 +16,26 @@ Rename-Item -Path 'c:\temp\fwtest\a.txt' -NewName 'c:\temp\fwtest\b.txt' Remove-Item -Path 'c:\temp\fwtest\b.txt' -Recurse ```` -## `create_and_remove_file_wsl2.csv` -Create and remove file in WSL 2. On file creation, a second "changed" event is written. +## `create_file_wsl2.csv` +Create file in WSL 2. On file creation, a second "changed" event is written. ````sh touch /mnt/c/temp/fwtest/a.txt -rm /mnt/c/temp/fwtest/a.txt +```` + + +## `create_and_rename_file_wsl2.csv` +Create and rename file in WSL 2. On file creation, a second "changed" event is written. +````sh +touch /mnt/c/temp/fwtest/a.txt +mv /mnt/c/temp/fwtest/a.txt /mnt/c/temp/fwtest/b.txt +```` + +## `create_rename_and_remove_file_wsl2.csv` +Create rename and remove file in WSL 2. On file creation, a second "changed" event is written. +````sh +touch /mnt/c/temp/fwtest/a.txt +mv /mnt/c/temp/fwtest/a.txt /mnt/c/temp/fwtest/b.txt +rm /mnt/c/temp/fwtest/b.txt ```` diff --git a/Source/FileWatcherExTests/scenario/create_and_remove_file.csv b/Source/FileWatcherExTests/scenario/create_and_remove_file.csv index dc18ae7..e19d087 100644 --- a/Source/FileWatcherExTests/scenario/create_and_remove_file.csv +++ b/Source/FileWatcherExTests/scenario/create_and_remove_file.csv @@ -1,3 +1,3 @@ -FullPath,EventName,OldFullPath,DiffInTicks,DiffInMilliseconds -C:\temp\fwtest\a.txt,created,,0,0 -C:\temp\fwtest\a.txt,deleted,,6170905,617 +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,a.txt,created,,0,0 +C:\temp\fwtest,a.txt,deleted,,8263221,826 diff --git a/Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv b/Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv deleted file mode 100644 index d0bd7bf..0000000 --- a/Source/FileWatcherExTests/scenario/create_and_remove_file_wsl2.csv +++ /dev/null @@ -1,4 +0,0 @@ -FullPath,EventName,OldFullPath,DiffInTicks,DiffInMilliseconds -C:\temp\fwtest\a.txt,created,,0,0 -C:\temp\fwtest\a.txt,changed,,7204,1 -C:\temp\fwtest\a.txt,deleted,,165845,17 diff --git a/Source/FileWatcherExTests/scenario/create_and_rename_file_wsl2.csv b/Source/FileWatcherExTests/scenario/create_and_rename_file_wsl2.csv new file mode 100644 index 0000000..0d92ec2 --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_and_rename_file_wsl2.csv @@ -0,0 +1,4 @@ +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,a.txt,created,,0,0 +C:\temp\fwtest,a.txt,changed,,5808,1 +C:\temp\fwtest,b.txt,renamed,a.txt,85490,9 diff --git a/Source/FileWatcherExTests/scenario/create_file_wsl2.csv b/Source/FileWatcherExTests/scenario/create_file_wsl2.csv new file mode 100644 index 0000000..5484ca5 --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_file_wsl2.csv @@ -0,0 +1,3 @@ +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,a.txt,created,,0,0 +C:\temp\fwtest,a.txt,changed,,13323,1 diff --git a/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv b/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv index d31825f..e296370 100644 --- a/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv +++ b/Source/FileWatcherExTests/scenario/create_rename_and_remove_file.csv @@ -1,4 +1,4 @@ Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds C:\temp\fwtest,a.txt,created,,0,0 -C:\temp\fwtest,b.txt,renamed,a.txt,1265338,127 -C:\temp\fwtest,b.txt,deleted,,6660690,666 +C:\temp\fwtest,b.txt,renamed,a.txt,1046536,105 +C:\temp\fwtest,b.txt,deleted,,4102153,410 diff --git a/Source/FileWatcherExTests/scenario/create_rename_and_remove_file_wsl2.csv b/Source/FileWatcherExTests/scenario/create_rename_and_remove_file_wsl2.csv new file mode 100644 index 0000000..688ece8 --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_rename_and_remove_file_wsl2.csv @@ -0,0 +1,5 @@ +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,a.txt,created,,0,0 +C:\temp\fwtest,a.txt,changed,,6136,1 +C:\temp\fwtest,b.txt,renamed,a.txt,94817,9 +C:\temp\fwtest,b.txt,deleted,,209770,21 From 254b658341966b376595b47844e9c78f094d7b4a Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 11:24:06 +0100 Subject: [PATCH 13/92] update README --- Source/FileWatcherExTests/scenario/README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Source/FileWatcherExTests/scenario/README.md b/Source/FileWatcherExTests/scenario/README.md index f5e776e..e2eef13 100644 --- a/Source/FileWatcherExTests/scenario/README.md +++ b/Source/FileWatcherExTests/scenario/README.md @@ -1,4 +1,19 @@ -## `create_file.csv` +# Scenarios For Integration Testing + +For each scenario: +- a fresh recording was started in an empty directory +- the listed commands were executed in a separate terminal +- the recording was stopped (CTRL + C) + +Then the recorded CSV files were used in integration tests using the `ReplayFileSystemWatcherWrapper.cs`. + +Example for starting a recording: +````powershell +PS C:\Projects\FileWatcherEx\Source\FileSystemEventRecorder> dotnet run C:\temp\fwtest\ C:\Projects\FileWatcherEx\Source\FileWatcherExTests\scenario\create_rename_and_remove_file_wsl2.csv +```` +# List of Scenarios + +## `create_file.csv` ````powershell New-Item -Path 'c:\temp\fwtest\a.txt' -ItemType File ```` From d479a553208b507b40a93e6894ac6977dd3bad3c Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 5 Dec 2022 11:32:24 +0100 Subject: [PATCH 14/92] wait time scenario --- .../FileWatcherExIntegrationTest.cs | 25 +++++++++++++++++++ Source/FileWatcherExTests/scenario/README.md | 12 ++++++++- ...me_and_remove_file_with_wait_time_wsl2.csv | 5 ++++ 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 Source/FileWatcherExTests/scenario/create_rename_and_remove_file_with_wait_time_wsl2.csv diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 6703c84..b0cfaaa 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -126,6 +126,31 @@ public void Create_Rename_And_Remove_Single_File_Via_WSL2() } + [Fact] + public void Create_Rename_And_Remove_Single_File_With_Wait_Time_Via_WSL2() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_rename_and_remove_file_with_wait_time_wsl2.csv"); + _fileWatcher.Stop(); + + Assert.Equal(3, _events.Count); + var ev1 = _events.ToList()[0]; + var ev2 = _events.ToList()[1]; + var ev3 = _events.ToList()[2]; + + Assert.Equal(ChangeType.CREATED, ev1.ChangeType); + Assert.Equal(@"C:\temp\fwtest\a.txt", ev1.FullPath); + + Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); + Assert.Equal(@"C:\temp\fwtest\b.txt", ev2.FullPath); + Assert.Equal(@"C:\temp\fwtest\a.txt", ev2.OldFullPath); + + Assert.Equal(ChangeType.DELETED, ev3.ChangeType); + Assert.Equal(@"C:\temp\fwtest\b.txt", ev3.FullPath); + Assert.Equal("", ev3.OldFullPath); + } + + [Fact(Skip = "requires real (Windows) file system")] public void SimpleRealFileSystemTest() { diff --git a/Source/FileWatcherExTests/scenario/README.md b/Source/FileWatcherExTests/scenario/README.md index e2eef13..f262ee3 100644 --- a/Source/FileWatcherExTests/scenario/README.md +++ b/Source/FileWatcherExTests/scenario/README.md @@ -46,11 +46,21 @@ mv /mnt/c/temp/fwtest/a.txt /mnt/c/temp/fwtest/b.txt ```` ## `create_rename_and_remove_file_wsl2.csv` -Create rename and remove file in WSL 2. On file creation, a second "changed" event is written. +Create, rename and remove file in WSL 2. On file creation, a second "changed" event is written. ````sh touch /mnt/c/temp/fwtest/a.txt mv /mnt/c/temp/fwtest/a.txt /mnt/c/temp/fwtest/b.txt rm /mnt/c/temp/fwtest/b.txt ```` +## `create_rename_and_remove_file_with_wait_time_wsl2.csv` +Create, rename and remove file in WSL 2. Additionally, some wait time is added. +````sh +touch /mnt/c/temp/fwtest/a.txt +sleep 1 +mv /mnt/c/temp/fwtest/a.txt /mnt/c/temp/fwtest/b.txt +sleep 1 +rm /mnt/c/temp/fwtest/b.txt +```` + diff --git a/Source/FileWatcherExTests/scenario/create_rename_and_remove_file_with_wait_time_wsl2.csv b/Source/FileWatcherExTests/scenario/create_rename_and_remove_file_with_wait_time_wsl2.csv new file mode 100644 index 0000000..153306e --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_rename_and_remove_file_with_wait_time_wsl2.csv @@ -0,0 +1,5 @@ +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,a.txt,created,,0,0 +C:\temp\fwtest,a.txt,changed,,6333,1 +C:\temp\fwtest,b.txt,renamed,a.txt,10362431,1036 +C:\temp\fwtest,b.txt,deleted,,10592668,1059 From 2d46cb5a9c351b1900b5c2672471f55b9a5ea9b9 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Wed, 7 Dec 2022 16:29:50 +0100 Subject: [PATCH 15/92] extract interface --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 2 +- Source/FileWatcherEx/IFileSystemWatcherEx.cs | 85 ++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 Source/FileWatcherEx/IFileSystemWatcherEx.cs diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 8bc5e02..8eb9d23 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -8,7 +8,7 @@ namespace FileWatcherEx; /// A wrapper of to standardize the events /// and avoid false change notifications. /// -public class FileSystemWatcherEx : IDisposable +public class FileSystemWatcherEx : IDisposable, IFileSystemWatcherEx { #region Private Properties diff --git a/Source/FileWatcherEx/IFileSystemWatcherEx.cs b/Source/FileWatcherEx/IFileSystemWatcherEx.cs new file mode 100644 index 0000000..ea7d1b8 --- /dev/null +++ b/Source/FileWatcherEx/IFileSystemWatcherEx.cs @@ -0,0 +1,85 @@ +using System.ComponentModel; + +namespace FileWatcherEx; + +public interface IFileSystemWatcherEx +{ + /// + /// Gets or sets the path of the directory to watch. + /// + string FolderPath { get; set; } + + /// + /// Gets the collection of all the filters used to determine what files are monitored in a directory. + /// + System.Collections.ObjectModel.Collection Filters { get; } + + /// + /// Gets or sets the filter string used to determine what files are monitored in a directory. + /// + string Filter { get; set; } + + /// + /// Gets or sets the type of changes to watch for. + /// The default is the bitwise OR combination of + /// , + /// , + /// and . + /// + NotifyFilters NotifyFilter { get; set; } + + /// + /// Gets or sets a value indicating whether subdirectories within the specified path should be monitored. + /// + bool IncludeSubdirectories { get; set; } + + /// + /// Gets or sets the object used to marshal the event handler calls issued as a result of a directory change. + /// + ISynchronizeInvoke? SynchronizingObject { get; set; } + + /// + /// Occurs when a file or directory in the specified + /// is changed. + /// + event FileSystemWatcherEx.DelegateOnChanged? OnChanged; + + /// + /// Occurs when a file or directory in the specified + /// is deleted. + /// + event FileSystemWatcherEx.DelegateOnDeleted? OnDeleted; + + /// + /// Occurs when a file or directory in the specified + /// is created. + /// + event FileSystemWatcherEx.DelegateOnCreated? OnCreated; + + /// + /// Occurs when a file or directory in the specified + /// is renamed. + /// + event FileSystemWatcherEx.DelegateOnRenamed? OnRenamed; + + /// + /// Occurs when the instance of is unable to continue + /// monitoring changes or when the internal buffer overflows. + /// + event FileSystemWatcherEx.DelegateOnError? OnError; + + /// + /// Start watching files + /// + void Start(); + + /// + /// Stop watching files + /// + void Stop(); + + /// + /// Dispose the FileWatcherEx instance + /// + void Dispose(); +} \ No newline at end of file From 646f197ea3055590cc49b576f5a56d65c1883c23 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Wed, 7 Dec 2022 16:34:15 +0100 Subject: [PATCH 16/92] newline --- Source/FileWatcherEx/IFileSystemWatcherEx.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/FileWatcherEx/IFileSystemWatcherEx.cs b/Source/FileWatcherEx/IFileSystemWatcherEx.cs index ea7d1b8..8da53ed 100644 --- a/Source/FileWatcherEx/IFileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/IFileSystemWatcherEx.cs @@ -82,4 +82,4 @@ public interface IFileSystemWatcherEx /// Dispose the FileWatcherEx instance /// void Dispose(); -} \ No newline at end of file +} From 06ffa2369b0f1d48738885f1084a8fc452152ff7 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 8 Dec 2022 13:51:34 +0100 Subject: [PATCH 17/92] cleanup --- Source/FileSystemEventRecorder/FileSystemEventRecorder.cs | 5 ++++- Source/FileWatcherEx/FileSystemWatcherEx.cs | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs index d969e09..ffe7df0 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs @@ -25,6 +25,9 @@ internal record EventRecordWithDiff( double DiffInMilliseconds // milliseconds between previous event and now. ); +/// +/// Command line tool to capture the raw events of the native FileSystemWatcher class in a CSV file +/// public static class FileSystemEventRecords { private static readonly ConcurrentQueue EventRecords = new(); @@ -33,7 +36,7 @@ public static void Main(string[] args) { var (watchedDirectory, csvOutputFile) = ProcessArguments(args); - var watcher = new FileSystemWatcherWrapper(); + var watcher = new FileSystemWatcher(); watcher.Path = watchedDirectory; watcher.IncludeSubdirectories = true; watcher.NotifyFilter = NotifyFilters.LastWrite diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index b35225f..406ae16 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -19,7 +19,7 @@ public class FileSystemWatcherEx : IDisposable private FileWatcher? _watcher; private IFileSystemWatcherWrapper? _fsw; - private readonly IFileSystemWatcherWrapper? _watcherWrapper; + private readonly IFileSystemWatcherWrapper? _injectedWatcherWrapper; // Define the cancellation token. private CancellationTokenSource? _cancelSource; @@ -133,7 +133,7 @@ public string Filter public FileSystemWatcherEx(string folderPath = "", IFileSystemWatcherWrapper? watcherWrapper = null) { FolderPath = folderPath; - _watcherWrapper = watcherWrapper; + _injectedWatcherWrapper = watcherWrapper; } @@ -263,7 +263,7 @@ void onError(ErrorEventArgs e) // Start watcher _watcher = new FileWatcher(); - _fsw = _watcher.Create(FolderPath, onEvent, onError, _watcherWrapper); + _fsw = _watcher.Create(FolderPath, onEvent, onError, _injectedWatcherWrapper); foreach (var filter in Filters) { From be8166e60d542d777ee548f4abe6c5a8504df5e7 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 8 Dec 2022 14:16:01 +0100 Subject: [PATCH 18/92] file creation via Windows explorer --- .../FileWatcherExIntegrationTest.cs | 45 +++++++++++++++++++ Source/FileWatcherExTests/scenario/README.md | 5 +++ .../create_and_rename_file_via_explorer.csv | 3 ++ ...te_rename_and_delete_file_via_explorer.csv | 4 ++ 4 files changed, 57 insertions(+) create mode 100644 Source/FileWatcherExTests/scenario/create_and_rename_file_via_explorer.csv create mode 100644 Source/FileWatcherExTests/scenario/create_rename_and_delete_file_via_explorer.csv diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index b0cfaaa..eebd78b 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -180,4 +180,49 @@ public void SimpleRealFileSystemTest() Assert.Equal(@"c:\temp\fwtest\b.txt", ev.FullPath); Assert.Equal("", ev.OldFullPath); } + + [Fact] + public void ManuallyCreateAndRenameFileViaWindowsExplorer() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_and_rename_file_via_explorer.csv"); + _fileWatcher.Stop(); + + Assert.Equal(2, _events.Count); + + var ev1 = _events.ToList()[0]; + var ev2 = _events.ToList()[1]; + + Assert.Equal(ChangeType.CREATED, ev1.ChangeType); + Assert.Equal(@"C:\temp\fwtest\New Text Document.txt", ev1.FullPath); + + Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); + Assert.Equal(@"C:\temp\fwtest\foo.txt", ev2.FullPath); + Assert.Equal(@"C:\temp\fwtest\New Text Document.txt", ev2.OldFullPath); + } + + [Fact] + public void ManuallyCreateRenameAndDeleteFileViaWindowsExplorer() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_rename_and_delete_file_via_explorer.csv"); + _fileWatcher.Stop(); + + Assert.Equal(3, _events.Count); + var ev1 = _events.ToList()[0]; + var ev2 = _events.ToList()[1]; + var ev3 = _events.ToList()[2]; + + Assert.Equal(ChangeType.CREATED, ev1.ChangeType); + Assert.Equal(@"C:\temp\fwtest\New Text Document.txt", ev1.FullPath); + + Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); + Assert.Equal(@"C:\temp\fwtest\foo.txt", ev2.FullPath); + Assert.Equal(@"C:\temp\fwtest\New Text Document.txt", ev2.OldFullPath); + + Assert.Equal(ChangeType.DELETED, ev3.ChangeType); + Assert.Equal(@"C:\temp\fwtest\foo.txt", ev3.FullPath); + Assert.Equal("", ev3.OldFullPath); + } + } \ No newline at end of file diff --git a/Source/FileWatcherExTests/scenario/README.md b/Source/FileWatcherExTests/scenario/README.md index f262ee3..c7bc601 100644 --- a/Source/FileWatcherExTests/scenario/README.md +++ b/Source/FileWatcherExTests/scenario/README.md @@ -63,4 +63,9 @@ sleep 1 rm /mnt/c/temp/fwtest/b.txt ```` +## `create_and_rename_file_via_explorer.csv` +Manually create a file in the Windows explorer. Change the default name "New Text Document.txt" +to "foo.txt". +## `create_rename_and_delete_file_via_explorer.csv` +Manually create, rename and delete a file in the Windows explorer. diff --git a/Source/FileWatcherExTests/scenario/create_and_rename_file_via_explorer.csv b/Source/FileWatcherExTests/scenario/create_and_rename_file_via_explorer.csv new file mode 100644 index 0000000..2143e79 --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_and_rename_file_via_explorer.csv @@ -0,0 +1,3 @@ +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,New Text Document.txt,created,,0,0 +C:\temp\fwtest,foo.txt,renamed,New Text Document.txt,15017686,1502 diff --git a/Source/FileWatcherExTests/scenario/create_rename_and_delete_file_via_explorer.csv b/Source/FileWatcherExTests/scenario/create_rename_and_delete_file_via_explorer.csv new file mode 100644 index 0000000..c763e1c --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_rename_and_delete_file_via_explorer.csv @@ -0,0 +1,4 @@ +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,New Text Document.txt,created,,0,0 +C:\temp\fwtest,foo.txt,renamed,New Text Document.txt,20053327,2005 +C:\temp\fwtest,foo.txt,deleted,,10305941,1031 From 5f46313d59a272debaeaa89a3a05ce8a525133d9 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 8 Dec 2022 14:25:31 +0100 Subject: [PATCH 19/92] edge browser download --- .../FileWatcherExIntegrationTest.cs | 18 ++++++++++++++++++ Source/FileWatcherExTests/scenario/README.md | 4 ++++ .../download_image_via_Edge_browser.csv | 5 +++++ 3 files changed, 27 insertions(+) create mode 100644 Source/FileWatcherExTests/scenario/download_image_via_Edge_browser.csv diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index eebd78b..72c65ba 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -225,4 +225,22 @@ public void ManuallyCreateRenameAndDeleteFileViaWindowsExplorer() Assert.Equal("", ev3.OldFullPath); } + [Fact] + public void DownloadImageViaEdgeBrowser() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\download_image_via_Edge_browser.csv"); + _fileWatcher.Stop(); + + Assert.Equal(2, _events.Count); + var ev1 = _events.ToList()[0]; + var ev2 = _events.ToList()[1]; + + Assert.Equal(ChangeType.CREATED, ev1.ChangeType); + Assert.Equal(@"C:\temp\fwtest\test.png.crdownload", ev1.FullPath); + + Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); + Assert.Equal(@"C:\temp\fwtest\test.png", ev2.FullPath); + Assert.Equal(@"C:\temp\fwtest\test.png.crdownload", ev2.OldFullPath); + } } \ No newline at end of file diff --git a/Source/FileWatcherExTests/scenario/README.md b/Source/FileWatcherExTests/scenario/README.md index c7bc601..81413ae 100644 --- a/Source/FileWatcherExTests/scenario/README.md +++ b/Source/FileWatcherExTests/scenario/README.md @@ -69,3 +69,7 @@ to "foo.txt". ## `create_rename_and_delete_file_via_explorer.csv` Manually create, rename and delete a file in the Windows explorer. + +## `download_image_via_Edge_browser.csv` +Download (right click, "Save image as") single image via Edge 106.0.1370.42. + diff --git a/Source/FileWatcherExTests/scenario/download_image_via_Edge_browser.csv b/Source/FileWatcherExTests/scenario/download_image_via_Edge_browser.csv new file mode 100644 index 0000000..0ad936e --- /dev/null +++ b/Source/FileWatcherExTests/scenario/download_image_via_Edge_browser.csv @@ -0,0 +1,5 @@ +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,test.png,created,,0,0 +C:\temp\fwtest,test.png,deleted,,8470,1 +C:\temp\fwtest,test.png.crdownload,created,,3352286,335 +C:\temp\fwtest,test.png,renamed,test.png.crdownload,1451627,145 From 768a009a1f18e3260de03ae02727d3674c523484 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 8 Dec 2022 15:30:35 +0100 Subject: [PATCH 20/92] create_subdirectory_add_and_remove_file --- .../FileWatcherExIntegrationTest.cs | 29 ++++++++++++++++++- Source/FileWatcherExTests/scenario/README.md | 7 +++++ ...reate_subdirectory_add_and_remove_file.csv | 7 +++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file.csv diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 72c65ba..3e7e178 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -4,7 +4,7 @@ namespace FileWatcherExTests; -public class FileWatcherExIntegrationTest +public class FileWatcherExIntegrationTest : IDisposable { private ConcurrentQueue _events; private ReplayFileSystemWatcherWrapper _replayer; @@ -243,4 +243,31 @@ public void DownloadImageViaEdgeBrowser() Assert.Equal(@"C:\temp\fwtest\test.png", ev2.FullPath); Assert.Equal(@"C:\temp\fwtest\test.png.crdownload", ev2.OldFullPath); } + + // this is probably an issue. When a directory is created and right after a file is created as well + // the file is filtered out although the original events contain it. + [Fact] + public void CreateSubDirectoryAddAndRemoveFile() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_subdirectory_add_and_remove_file.csv"); + _fileWatcher.Stop(); + + Assert.Equal(2, _events.Count); + var ev1 = _events.ToList()[0]; + var ev2 = _events.ToList()[1]; + + Assert.Equal(ChangeType.CREATED, ev1.ChangeType); + Assert.Equal(@"C:\temp\fwtest\subdir", ev1.FullPath); + + Assert.Equal(ChangeType.CHANGED, ev2.ChangeType); + Assert.Equal(@"C:\temp\fwtest\subdir", ev2.FullPath); + Assert.Equal(@"", ev2.OldFullPath); + } + + // cleanup + public void Dispose() + { + _fileWatcher.Dispose(); + } } \ No newline at end of file diff --git a/Source/FileWatcherExTests/scenario/README.md b/Source/FileWatcherExTests/scenario/README.md index 81413ae..049adf8 100644 --- a/Source/FileWatcherExTests/scenario/README.md +++ b/Source/FileWatcherExTests/scenario/README.md @@ -73,3 +73,10 @@ Manually create, rename and delete a file in the Windows explorer. ## `download_image_via_Edge_browser.csv` Download (right click, "Save image as") single image via Edge 106.0.1370.42. +## `create_subdirectory_add_and_remove_file.csv` +Create, rename and remove file in WSL 2. Additionally, some wait time is added. +````sh +mkdir -p /mnt/c/temp/fwtest/subdir/ +touch /mnt/c/temp/fwtest/subdir/a.txt +rm /mnt/c/temp/fwtest/subdir/a.txt +```` diff --git a/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file.csv b/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file.csv new file mode 100644 index 0000000..cc8edb1 --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file.csv @@ -0,0 +1,7 @@ +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,subdir,created,,0,0 +C:\temp\fwtest\subdir,a.txt,created,,102207,10 +C:\temp\fwtest\subdir,a.txt,changed,,33570,3 +C:\temp\fwtest,subdir,changed,,139729,14 +C:\temp\fwtest\subdir,a.txt,deleted,,216095,22 +C:\temp\fwtest,subdir,changed,,5751702,575 From 4ea7da7509f0ae228b98b93731c5f557c279eb7e Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 8 Dec 2022 15:49:28 +0100 Subject: [PATCH 21/92] create and remove file with sleep in sub dir --- .../FileWatcherExIntegrationTest.cs | 33 +++++++++++++++++-- Source/FileWatcherExTests/scenario/README.md | 11 ++++++- ...rectory_add_and_remove_file_with_sleep.csv | 6 ++++ 3 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file_with_sleep.csv diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 3e7e178..ee85a84 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -244,8 +244,7 @@ public void DownloadImageViaEdgeBrowser() Assert.Equal(@"C:\temp\fwtest\test.png.crdownload", ev2.OldFullPath); } - // this is probably an issue. When a directory is created and right after a file is created as well - // the file is filtered out although the original events contain it. + // instantly removed file is not in the events list [Fact] public void CreateSubDirectoryAddAndRemoveFile() { @@ -265,6 +264,36 @@ public void CreateSubDirectoryAddAndRemoveFile() Assert.Equal(@"", ev2.OldFullPath); } + [Fact] + public void CreateSubDirectoryAddAndRemoveFileWithSleep() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_subdirectory_add_and_remove_file_with_sleep.csv"); + _fileWatcher.Stop(); + + Assert.Equal(4, _events.Count); + var ev1 = _events.ToList()[0]; + var ev2 = _events.ToList()[1]; + var ev3 = _events.ToList()[2]; + var ev4 = _events.ToList()[3]; + + Assert.Equal(ChangeType.CREATED, ev1.ChangeType); + Assert.Equal(@"C:\temp\fwtest\subdir", ev1.FullPath); + + Assert.Equal(ChangeType.CREATED, ev2.ChangeType); + Assert.Equal(@"C:\temp\fwtest\subdir\a.txt", ev2.FullPath); + Assert.Equal(@"", ev2.OldFullPath); + + // TODO this could be filtered out + Assert.Equal(ChangeType.CHANGED, ev3.ChangeType); + Assert.Equal(@"C:\temp\fwtest\subdir", ev3.FullPath); + Assert.Equal(@"", ev3.OldFullPath); + + Assert.Equal(ChangeType.DELETED, ev4.ChangeType); + Assert.Equal(@"C:\temp\fwtest\subdir\a.txt", ev4.FullPath); + Assert.Equal(@"", ev4.OldFullPath); + } + // cleanup public void Dispose() { diff --git a/Source/FileWatcherExTests/scenario/README.md b/Source/FileWatcherExTests/scenario/README.md index 049adf8..2ef652f 100644 --- a/Source/FileWatcherExTests/scenario/README.md +++ b/Source/FileWatcherExTests/scenario/README.md @@ -74,9 +74,18 @@ Manually create, rename and delete a file in the Windows explorer. Download (right click, "Save image as") single image via Edge 106.0.1370.42. ## `create_subdirectory_add_and_remove_file.csv` -Create, rename and remove file in WSL 2. Additionally, some wait time is added. +In WSL2, create a subdirectory, create and remove a file. +````sh +mkdir -p /mnt/c/temp/fwtest/subdir/ +touch /mnt/c/temp/fwtest/subdir/a.txt +rm /mnt/c/temp/fwtest/subdir/a.txt +```` + +## `create_subdirectory_add_and_remove_file_with_sleep.csv` +In WSL2, create a subdirectory, Create the file, wait for a while, then remove it. ````sh mkdir -p /mnt/c/temp/fwtest/subdir/ touch /mnt/c/temp/fwtest/subdir/a.txt +sleep 1 rm /mnt/c/temp/fwtest/subdir/a.txt ```` diff --git a/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file_with_sleep.csv b/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file_with_sleep.csv new file mode 100644 index 0000000..c349beb --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file_with_sleep.csv @@ -0,0 +1,6 @@ +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,subdir,created,,0,0 +C:\temp\fwtest\subdir,a.txt,created,,6194,1 +C:\temp\fwtest\subdir,a.txt,changed,,3000,0 +C:\temp\fwtest,subdir,changed,,5187887,519 +C:\temp\fwtest\subdir,a.txt,deleted,,5201725,520 From f761bd9fadb1e825c49de6e0bb5682e04e658bb0 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 8 Dec 2022 17:03:16 +0100 Subject: [PATCH 22/92] stub out symbolic link test --- .../FileWatcherExIntegrationTest.cs | 14 ++++++++++++++ Source/FileWatcherExTests/scenario/README.md | 10 ++++++++++ .../create_file_inside_symbolic_link_directory.csv | 7 +++++++ 3 files changed, 31 insertions(+) create mode 100644 Source/FileWatcherExTests/scenario/create_file_inside_symbolic_link_directory.csv diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index ee85a84..747dbae 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -294,6 +294,20 @@ public void CreateSubDirectoryAddAndRemoveFileWithSleep() Assert.Equal(@"", ev4.OldFullPath); } + // TODO This scenario tries to test the code paths checking for a "reparse point" + // which is a symbolic link in NTFS: https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-points + // Currently, the test setup does not support this. Namely, calls to File.GetAttributes(...) and + // File.GetAttributes(...) would need to be wrapped and passed in e.g. as a Func + [Fact (Skip = "test setup needs to be extended")] + public void CreateFileInsideSymbolicLinkDirectory() + { + _fileWatcher.Start(); + _replayer.Replay(@"scenario\create_file_inside_symbolic_link_directory.csv"); + _fileWatcher.Stop(); + + Assert.Equal(6, _events.Count); + } + // cleanup public void Dispose() { diff --git a/Source/FileWatcherExTests/scenario/README.md b/Source/FileWatcherExTests/scenario/README.md index 2ef652f..ed0dfdf 100644 --- a/Source/FileWatcherExTests/scenario/README.md +++ b/Source/FileWatcherExTests/scenario/README.md @@ -89,3 +89,13 @@ touch /mnt/c/temp/fwtest/subdir/a.txt sleep 1 rm /mnt/c/temp/fwtest/subdir/a.txt ```` + +## `create_file_inside_symbolic_link_directory` +Create subdir, create symbolic link to it and then place file inside this symbolic link dir. + +````powershell +New-Item –itemType Directory -Path 'c:\temp\fwtest\subdir' +New-Item -ItemType Junction -Path 'c:\temp\fwtest\symlink_subdir' -Target 'c:\temp\fwtest\subdir' +New-Item -ItemType File -Path 'c:\temp\fwtest\symlink_subdir\foo.txt' +Remove-Item -Path 'c:\temp\fwtest\symlink_subdir\foo.txt' -Recurse +```` diff --git a/Source/FileWatcherExTests/scenario/create_file_inside_symbolic_link_directory.csv b/Source/FileWatcherExTests/scenario/create_file_inside_symbolic_link_directory.csv new file mode 100644 index 0000000..c2fe455 --- /dev/null +++ b/Source/FileWatcherExTests/scenario/create_file_inside_symbolic_link_directory.csv @@ -0,0 +1,7 @@ +Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds +C:\temp\fwtest,subdir,created,,0,0 +C:\temp\fwtest,symlink_subdir,created,,287271,29 +C:\temp\fwtest\subdir,foo.txt,created,,571620,57 +C:\temp\fwtest,subdir,changed,,7601128,760 +C:\temp\fwtest\subdir,foo.txt,deleted,,5158,1 +C:\temp\fwtest,subdir,changed,,1527420,153 From 571ec2d8f64f43a8b9c67b1d5224530c6de4395a Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 8 Dec 2022 17:12:50 +0100 Subject: [PATCH 23/92] introduce recording dir --- Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 747dbae..7b85787 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -16,7 +16,9 @@ public FileWatcherExIntegrationTest() _events = new(); _replayer = new(); var unusedDir = Path.GetTempPath(); - _fileWatcher = new FileSystemWatcherEx(unusedDir, _replayer); + + const string recordingDir = @"C:\temp\fwtest"; + _fileWatcher = new FileSystemWatcherEx(recordingDir, _replayer); _fileWatcher.OnCreated += (_, ev) => _events.Enqueue(ev); _fileWatcher.OnDeleted += (_, ev) => _events.Enqueue(ev); @@ -24,7 +26,6 @@ public FileWatcherExIntegrationTest() _fileWatcher.OnRenamed += (_, ev) => _events.Enqueue(ev); } - [Fact] public void Create_Single_File() { From f52b13545d8a48f4ac448e93e04a7f7ab94356cc Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 8 Dec 2022 17:14:07 +0100 Subject: [PATCH 24/92] comment --- Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 7b85787..0eeb8fe 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -4,6 +4,10 @@ namespace FileWatcherExTests; +/// +/// Integration/ Golden master test for FileWatcherEx +/// Considers C:\temp\fwtest to be the test directory +/// public class FileWatcherExIntegrationTest : IDisposable { private ConcurrentQueue _events; From 7d3a9f741c663fd3d30fde082c023629f7a67265 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sat, 10 Dec 2022 15:06:06 +0100 Subject: [PATCH 25/92] update comment --- Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 0eeb8fe..0566897 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -301,7 +301,7 @@ public void CreateSubDirectoryAddAndRemoveFileWithSleep() // TODO This scenario tries to test the code paths checking for a "reparse point" // which is a symbolic link in NTFS: https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-points - // Currently, the test setup does not support this. Namely, calls to File.GetAttributes(...) and + // Currently, the test setup does not support this. Namely, calls to new DirectoryInfo(path).GetDirectories() and // File.GetAttributes(...) would need to be wrapped and passed in e.g. as a Func [Fact (Skip = "test setup needs to be extended")] public void CreateFileInsideSymbolicLinkDirectory() From 8518ab0e0b71a86410274ad4cf32f0b4f00d556d Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sat, 10 Dec 2022 16:35:32 +0100 Subject: [PATCH 26/92] inject IO functions --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 36 +++++++++++++------ .../FileWatcherExIntegrationTest.cs | 2 ++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 0ebedfb..805c131 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -4,14 +4,26 @@ namespace FileWatcherEx; - internal class FileWatcher : IDisposable { private string _watchPath = string.Empty; private Action? _eventCallback = null; private readonly Dictionary _fwDictionary = new(); private Action? _onError = null; + private Func? _getFileAttributesFunc; + private Func? _getDirectoryInfosFunc; + + internal Func GetFileAttributesFunc + { + get => _getFileAttributesFunc ?? (p => File.GetAttributes(p)); + set => _getFileAttributesFunc = value; + } + internal Func GetDirectoryInfosFunc + { + get => _getDirectoryInfosFunc ?? (p => new DirectoryInfo(p).GetDirectories()); + set => _getDirectoryInfosFunc = value; + } /// /// Create new instance of FileSystemWatcherWrapper @@ -24,7 +36,6 @@ internal class FileWatcher : IDisposable public IFileSystemWatcherWrapper Create(string path, Action onEvent, Action onError, IFileSystemWatcherWrapper? watcher = null) { _watchPath = path; - _eventCallback = onEvent; _onError = onError; watcher ??= new FileSystemWatcherWrapper(); @@ -49,9 +60,9 @@ public IFileSystemWatcherWrapper Create(string path, Action on _fwDictionary.Add(path, watcher); // this handles sub directories. Probably needs cleanup - foreach (var dirInfo in new DirectoryInfo(path).GetDirectories()) + foreach (var dirInfo in GetDirectoryInfosFunc(path)) { - var attrs = File.GetAttributes(dirInfo.FullName); + var attrs = GetFileAttributesFunc(dirInfo.FullName); // TODO: consider skipping hidden/system folders? // See IG Issue #405 comment below @@ -120,18 +131,21 @@ private void MakeWatcher(string path) fileSystemWatcherRoot.Created += new(MakeWatcher_Created); fileSystemWatcherRoot.Deleted += new(MakeWatcher_Deleted); - fileSystemWatcherRoot.Changed += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CHANGED)); - fileSystemWatcherRoot.Created += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CREATED)); - fileSystemWatcherRoot.Deleted += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.DELETED)); + fileSystemWatcherRoot.Changed += + new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CHANGED)); + fileSystemWatcherRoot.Created += + new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CREATED)); + fileSystemWatcherRoot.Deleted += + new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.DELETED)); fileSystemWatcherRoot.Renamed += new((object _, RenamedEventArgs e) => ProcessEvent(e)); fileSystemWatcherRoot.Error += new((object _, ErrorEventArgs e) => _onError?.Invoke(e)); _fwDictionary.Add(path, fileSystemWatcherRoot); } - foreach (var item in new DirectoryInfo(path).GetDirectories()) + foreach (var item in GetDirectoryInfosFunc(path)) { - var attrs = File.GetAttributes(item.FullName); + var attrs = GetFileAttributesFunc(item.FullName); // If is a directory and symbolic link if (attrs.HasFlag(FileAttributes.Directory) @@ -169,7 +183,7 @@ private void MakeWatcher_Created(object sender, FileSystemEventArgs e) { try { - var attrs = File.GetAttributes(e.FullPath); + var attrs = GetFileAttributesFunc(e.FullPath); if (attrs.HasFlag(FileAttributes.Directory) && attrs.HasFlag(FileAttributes.ReparsePoint)) { @@ -221,4 +235,4 @@ public void Dispose() item.Value.Dispose(); } } -} +} \ No newline at end of file diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 0566897..4892e35 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -4,6 +4,8 @@ namespace FileWatcherExTests; +// TODO subdirectory tests + /// /// Integration/ Golden master test for FileWatcherEx /// Considers C:\temp\fwtest to be the test directory From d7d346e214f33e7315b7da3d261e635d552e92f3 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sat, 10 Dec 2022 17:55:03 +0100 Subject: [PATCH 27/92] inject IO functions --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 12 +- Source/FileWatcherEx/Helpers/FileWatcher.cs | 1 + .../FileWatcherExIntegrationTest.cs | 125 ++++++++---------- 3 files changed, 67 insertions(+), 71 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 406ae16..ca86fa6 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -79,8 +79,6 @@ public string Filter #endregion - - #region Public Events /// @@ -278,6 +276,16 @@ void onError(ErrorEventArgs e) _fsw.EnableRaisingEvents = true; } + internal void StartForTesting( + Func getFileAttributesFunc, + Func getDirectoryInfosFunc) + { + Start(); + if (_watcher is null) return; + _watcher.GetFileAttributesFunc = getFileAttributesFunc; + _watcher.GetDirectoryInfosFunc = getDirectoryInfosFunc; + } + /// /// Stop watching files diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 805c131..131d7a8 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -36,6 +36,7 @@ internal Func GetDirectoryInfosFunc public IFileSystemWatcherWrapper Create(string path, Action onEvent, Action onError, IFileSystemWatcherWrapper? watcher = null) { _watchPath = path; + _eventCallback = onEvent; _onError = onError; watcher ??= new FileSystemWatcherWrapper(); diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 4892e35..6f6cb35 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -21,7 +21,6 @@ public FileWatcherExIntegrationTest() // setup before each test run _events = new(); _replayer = new(); - var unusedDir = Path.GetTempPath(); const string recordingDir = @"C:\temp\fwtest"; _fileWatcher = new FileSystemWatcherEx(recordingDir, _replayer); @@ -35,9 +34,7 @@ public FileWatcherExIntegrationTest() [Fact] public void Create_Single_File() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_file.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\create_file.csv"); Assert.Single(_events); var ev = _events.First(); @@ -49,9 +46,7 @@ public void Create_Single_File() [Fact] public void Create_And_Remove_Single_File() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_and_remove_file.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\create_and_remove_file.csv"); Assert.Equal(2, _events.Count); var ev1 = _events.ToList()[0]; @@ -70,9 +65,7 @@ public void Create_And_Remove_Single_File() [Fact] public void Create_Rename_And_Remove_Single_File() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_rename_and_remove_file.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\create_rename_and_remove_file.csv"); Assert.Equal(3, _events.Count); var ev1 = _events.ToList()[0]; @@ -95,9 +88,7 @@ public void Create_Rename_And_Remove_Single_File() // filters out 2nd "changed" event public void Create_Single_File_Via_WSL2() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_file_wsl2.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\create_file_wsl2.csv"); Assert.Single(_events); var ev = _events.First(); @@ -105,15 +96,13 @@ public void Create_Single_File_Via_WSL2() Assert.Equal(@"C:\temp\fwtest\a.txt", ev.FullPath); Assert.Equal("", ev.OldFullPath); } - + [Fact] // scenario creates "created" "changed" and "renamed" event. // resulting event is just "created" with the filename taken from "renamed" public void Create_And_Rename_Single_File_Via_WSL2() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_and_rename_file_wsl2.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\create_and_rename_file_wsl2.csv"); Assert.Single(_events); var ev = _events.First(); @@ -125,10 +114,7 @@ public void Create_And_Rename_Single_File_Via_WSL2() [Fact] public void Create_Rename_And_Remove_Single_File_Via_WSL2() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_rename_and_remove_file_wsl2.csv"); - _fileWatcher.Stop(); - + StartFileWatcherAndReplay(@"scenario\create_rename_and_remove_file_wsl2.csv"); Assert.Empty(_events); } @@ -136,9 +122,7 @@ public void Create_Rename_And_Remove_Single_File_Via_WSL2() [Fact] public void Create_Rename_And_Remove_Single_File_With_Wait_Time_Via_WSL2() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_rename_and_remove_file_with_wait_time_wsl2.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\create_rename_and_remove_file_with_wait_time_wsl2.csv"); Assert.Equal(3, _events.Count); var ev1 = _events.ToList()[0]; @@ -156,44 +140,12 @@ public void Create_Rename_And_Remove_Single_File_With_Wait_Time_Via_WSL2() Assert.Equal(@"C:\temp\fwtest\b.txt", ev3.FullPath); Assert.Equal("", ev3.OldFullPath); } - - [Fact(Skip = "requires real (Windows) file system")] - public void SimpleRealFileSystemTest() - { - ConcurrentQueue events = new(); - var fw = new FileSystemWatcherEx(@"c:\temp\fwtest\"); - - fw.OnCreated += (_, ev) => events.Enqueue(ev); - fw.OnDeleted += (_, ev) => events.Enqueue(ev); - fw.OnChanged += (_, ev) => events.Enqueue(ev); - fw.OnRenamed += (_, ev) => events.Enqueue(ev); - fw.OnRenamed += (_, ev) => events.Enqueue(ev); - - const string testFile = @"c:\temp\fwtest\b.txt"; - if (File.Exists(testFile)) - { - File.Delete(testFile); - } - - fw.Start(); - File.Create(testFile); - Thread.Sleep(250); - fw.Stop(); - - Assert.Single(events); - var ev = events.First(); - Assert.Equal(ChangeType.CREATED, ev.ChangeType); - Assert.Equal(@"c:\temp\fwtest\b.txt", ev.FullPath); - Assert.Equal("", ev.OldFullPath); - } [Fact] public void ManuallyCreateAndRenameFileViaWindowsExplorer() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_and_rename_file_via_explorer.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\create_and_rename_file_via_explorer.csv"); Assert.Equal(2, _events.Count); @@ -211,9 +163,7 @@ public void ManuallyCreateAndRenameFileViaWindowsExplorer() [Fact] public void ManuallyCreateRenameAndDeleteFileViaWindowsExplorer() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_rename_and_delete_file_via_explorer.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\create_rename_and_delete_file_via_explorer.csv"); Assert.Equal(3, _events.Count); var ev1 = _events.ToList()[0]; @@ -235,9 +185,7 @@ public void ManuallyCreateRenameAndDeleteFileViaWindowsExplorer() [Fact] public void DownloadImageViaEdgeBrowser() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\download_image_via_Edge_browser.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\download_image_via_Edge_browser.csv"); Assert.Equal(2, _events.Count); var ev1 = _events.ToList()[0]; @@ -255,9 +203,7 @@ public void DownloadImageViaEdgeBrowser() [Fact] public void CreateSubDirectoryAddAndRemoveFile() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_subdirectory_add_and_remove_file.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\create_subdirectory_add_and_remove_file.csv"); Assert.Equal(2, _events.Count); var ev1 = _events.ToList()[0]; @@ -274,9 +220,7 @@ public void CreateSubDirectoryAddAndRemoveFile() [Fact] public void CreateSubDirectoryAddAndRemoveFileWithSleep() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_subdirectory_add_and_remove_file_with_sleep.csv"); - _fileWatcher.Stop(); + StartFileWatcherAndReplay(@"scenario\create_subdirectory_add_and_remove_file_with_sleep.csv"); Assert.Equal(4, _events.Count); var ev1 = _events.ToList()[0]; @@ -314,10 +258,53 @@ public void CreateFileInsideSymbolicLinkDirectory() Assert.Equal(6, _events.Count); } + + [Fact(Skip = "requires real (Windows) file system")] + public void SimpleRealFileSystemTest() + { + ConcurrentQueue events = new(); + var fw = new FileSystemWatcherEx(@"c:\temp\fwtest\"); + + fw.OnCreated += (_, ev) => events.Enqueue(ev); + fw.OnDeleted += (_, ev) => events.Enqueue(ev); + fw.OnChanged += (_, ev) => events.Enqueue(ev); + fw.OnRenamed += (_, ev) => events.Enqueue(ev); + fw.OnRenamed += (_, ev) => events.Enqueue(ev); + const string testFile = @"c:\temp\fwtest\b.txt"; + if (File.Exists(testFile)) + { + File.Delete(testFile); + } + + _fileWatcher.StartForTesting( + p => FileAttributes.Normal, + p => Array.Empty()); + File.Create(testFile); + Thread.Sleep(250); + fw.Stop(); + + Assert.Single(events); + var ev = events.First(); + Assert.Equal(ChangeType.CREATED, ev.ChangeType); + Assert.Equal(@"c:\temp\fwtest\b.txt", ev.FullPath); + Assert.Equal("", ev.OldFullPath); + } + // cleanup public void Dispose() { _fileWatcher.Dispose(); } + + private void StartFileWatcherAndReplay(string csvFile) + { + _fileWatcher.StartForTesting( + p => FileAttributes.Normal, + // only used for FullName + p => new[] { new DirectoryInfo(p)}); + _replayer.Replay(csvFile); + _fileWatcher.Stop(); + } + } \ No newline at end of file From e6a3941a49d4d5155a85a9e074686a8d53df8fba Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sat, 10 Dec 2022 18:12:08 +0100 Subject: [PATCH 28/92] adjust subdir test --- .../FileWatcherExTests/FileWatcherExIntegrationTest.cs | 6 ++++++ .../create_subdirectory_add_and_remove_file.csv | 10 +++++----- ...ate_subdirectory_add_and_remove_file_with_sleep.csv | 8 ++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 6f6cb35..0f51144 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -24,6 +24,7 @@ public FileWatcherExIntegrationTest() const string recordingDir = @"C:\temp\fwtest"; _fileWatcher = new FileSystemWatcherEx(recordingDir, _replayer); + _fileWatcher.IncludeSubdirectories = true; _fileWatcher.OnCreated += (_, ev) => _events.Enqueue(ev); _fileWatcher.OnDeleted += (_, ev) => _events.Enqueue(ev); @@ -31,6 +32,7 @@ public FileWatcherExIntegrationTest() _fileWatcher.OnRenamed += (_, ev) => _events.Enqueue(ev); } + [Fact] public void Create_Single_File() { @@ -42,6 +44,7 @@ public void Create_Single_File() Assert.Equal(@"C:\temp\fwtest\a.txt", ev.FullPath); Assert.Equal("", ev.OldFullPath); } + [Fact] public void Create_And_Remove_Single_File() @@ -84,6 +87,7 @@ public void Create_Rename_And_Remove_Single_File() Assert.Equal("", ev3.OldFullPath); } + [Fact] // filters out 2nd "changed" event public void Create_Single_File_Via_WSL2() @@ -97,6 +101,7 @@ public void Create_Single_File_Via_WSL2() Assert.Equal("", ev.OldFullPath); } + [Fact] // scenario creates "created" "changed" and "renamed" event. // resulting event is just "created" with the filename taken from "renamed" @@ -111,6 +116,7 @@ public void Create_And_Rename_Single_File_Via_WSL2() Assert.Null(ev.OldFullPath); } + [Fact] public void Create_Rename_And_Remove_Single_File_Via_WSL2() { diff --git a/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file.csv b/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file.csv index cc8edb1..aa953e1 100644 --- a/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file.csv +++ b/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file.csv @@ -1,7 +1,7 @@ Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds C:\temp\fwtest,subdir,created,,0,0 -C:\temp\fwtest\subdir,a.txt,created,,102207,10 -C:\temp\fwtest\subdir,a.txt,changed,,33570,3 -C:\temp\fwtest,subdir,changed,,139729,14 -C:\temp\fwtest\subdir,a.txt,deleted,,216095,22 -C:\temp\fwtest,subdir,changed,,5751702,575 +C:\temp\fwtest\subdir,a.txt,created,,52133,5 +C:\temp\fwtest\subdir,a.txt,changed,,21171,2 +C:\temp\fwtest,subdir,changed,,132289,13 +C:\temp\fwtest\subdir,a.txt,deleted,,226731,23 +C:\temp\fwtest,subdir,changed,,2123902,212 diff --git a/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file_with_sleep.csv b/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file_with_sleep.csv index c349beb..7a9148c 100644 --- a/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file_with_sleep.csv +++ b/Source/FileWatcherExTests/scenario/create_subdirectory_add_and_remove_file_with_sleep.csv @@ -1,6 +1,6 @@ Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds C:\temp\fwtest,subdir,created,,0,0 -C:\temp\fwtest\subdir,a.txt,created,,6194,1 -C:\temp\fwtest\subdir,a.txt,changed,,3000,0 -C:\temp\fwtest,subdir,changed,,5187887,519 -C:\temp\fwtest\subdir,a.txt,deleted,,5201725,520 +C:\temp\fwtest\subdir,a.txt,created,,371990,37 +C:\temp\fwtest\subdir,a.txt,changed,,78301,8 +C:\temp\fwtest,subdir,changed,,7198911,720 +C:\temp\fwtest\subdir,a.txt,deleted,,3310510,331 From d96b9a2b4151323950c10cd0d5578bc816276de6 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sat, 10 Dec 2022 18:38:18 +0100 Subject: [PATCH 29/92] process subdirs in demo --- Source/Demo/Form1.cs | 3 ++- Source/FileWatcherEx/Helpers/FileWatcher.cs | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Demo/Form1.cs b/Source/Demo/Form1.cs index bb5f772..ba1b0f0 100644 --- a/Source/Demo/Form1.cs +++ b/Source/Demo/Form1.cs @@ -25,7 +25,8 @@ private void BtnStart_Click(object sender, EventArgs e) _fw.OnError += FW_OnError; _fw.SynchronizingObject = this; - + _fw.IncludeSubdirectories = true; + _fw.Start(); btnStart.Enabled = false; diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 131d7a8..e2fb73b 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -58,6 +58,7 @@ public IFileSystemWatcherWrapper Create(string path, Action on //changing this to a higher value can lead into issues when watching UNC drives watcher.InternalBufferSize = 32768; + // root watcher _fwDictionary.Add(path, watcher); // this handles sub directories. Probably needs cleanup From ad09e0757d83af5f20ca9dd2864467e72de6ba73 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sun, 11 Dec 2022 13:38:53 +0100 Subject: [PATCH 30/92] towards injectable fsw factory --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 19 ++++++++++++------- Source/FileWatcherEx/Helpers/FileWatcher.cs | 8 +++++--- .../FileWatcherExIntegrationTest.cs | 3 ++- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index ca86fa6..12d69e7 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -10,7 +10,8 @@ namespace FileWatcherEx; /// public class FileSystemWatcherEx : IDisposable { - + private readonly Func? _watcherFactory; + #region Private Properties private Thread? _thread; @@ -19,15 +20,21 @@ public class FileSystemWatcherEx : IDisposable private FileWatcher? _watcher; private IFileSystemWatcherWrapper? _fsw; - private readonly IFileSystemWatcherWrapper? _injectedWatcherWrapper; + private Func? _fswFactory; // Define the cancellation token. private CancellationTokenSource? _cancelSource; + // how a file watcher is injected + internal Func FileSystemWatcherFactory + { + get => _fswFactory ?? (() => new FileSystemWatcherWrapper()); + set => _fswFactory = value; + } + #endregion - #region Public Properties /// @@ -127,11 +134,9 @@ public string Filter /// Initialize new instance of /// /// - /// optional watcher wrapper, used to inject fake implementation - public FileSystemWatcherEx(string folderPath = "", IFileSystemWatcherWrapper? watcherWrapper = null) + public FileSystemWatcherEx(string folderPath = "") { FolderPath = folderPath; - _injectedWatcherWrapper = watcherWrapper; } @@ -261,7 +266,7 @@ void onError(ErrorEventArgs e) // Start watcher _watcher = new FileWatcher(); - _fsw = _watcher.Create(FolderPath, onEvent, onError, _injectedWatcherWrapper); + _fsw = _watcher.Create(FolderPath, onEvent, onError, FileSystemWatcherFactory); foreach (var filter in Filters) { diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index e2fb73b..316b865 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -12,6 +12,7 @@ internal class FileWatcher : IDisposable private Action? _onError = null; private Func? _getFileAttributesFunc; private Func? _getDirectoryInfosFunc; + private Func _watcherFactory; internal Func GetFileAttributesFunc { @@ -31,15 +32,16 @@ internal Func GetDirectoryInfosFunc /// Full folder path to watcher /// onEvent callback /// onError callback - /// + /// how to create a FileSystemWatcher /// - public IFileSystemWatcherWrapper Create(string path, Action onEvent, Action onError, IFileSystemWatcherWrapper? watcher = null) + public IFileSystemWatcherWrapper Create(string path, Action onEvent, Action onError, Func watcherFactory) { + _watcherFactory = watcherFactory; _watchPath = path; _eventCallback = onEvent; _onError = onError; - watcher ??= new FileSystemWatcherWrapper(); + var watcher = watcherFactory(); watcher.Path = _watchPath; watcher.IncludeSubdirectories = true; watcher.NotifyFilter = NotifyFilters.LastWrite diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 0f51144..c6f601f 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -23,7 +23,8 @@ public FileWatcherExIntegrationTest() _replayer = new(); const string recordingDir = @"C:\temp\fwtest"; - _fileWatcher = new FileSystemWatcherEx(recordingDir, _replayer); + _fileWatcher = new FileSystemWatcherEx(recordingDir); + _fileWatcher.FileSystemWatcherFactory = () => _replayer; _fileWatcher.IncludeSubdirectories = true; _fileWatcher.OnCreated += (_, ev) => _events.Enqueue(ev); From 280a96c086a2d08bdf1f3dec1a672d6d8320752d Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sun, 11 Dec 2022 17:07:56 +0100 Subject: [PATCH 31/92] using watcher factory --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 316b865..cbc9cb6 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -124,12 +124,10 @@ private void MakeWatcher(string path) { if (!_fwDictionary.ContainsKey(path)) { - var fileSystemWatcherRoot = new FileSystemWatcherWrapper - { - Path = path, - IncludeSubdirectories = true, - EnableRaisingEvents = true - }; + var fileSystemWatcherRoot = _watcherFactory(); + fileSystemWatcherRoot.Path = path; + fileSystemWatcherRoot.IncludeSubdirectories = true; + fileSystemWatcherRoot.EnableRaisingEvents = true; // Bind internal events to manipulate the possible symbolic links fileSystemWatcherRoot.Created += new(MakeWatcher_Created); @@ -191,12 +189,10 @@ private void MakeWatcher_Created(object sender, FileSystemEventArgs e) if (attrs.HasFlag(FileAttributes.Directory) && attrs.HasFlag(FileAttributes.ReparsePoint)) { - var watcherCreated = new FileSystemWatcherWrapper - { - Path = e.FullPath, - IncludeSubdirectories = true, - EnableRaisingEvents = true - }; + var watcherCreated = _watcherFactory(); + watcherCreated.Path = e.FullPath; + watcherCreated.IncludeSubdirectories = true; + watcherCreated.EnableRaisingEvents = true; // Bind internal events to manipulate the possible symbolic links watcherCreated.Created += new(MakeWatcher_Created); From 957185eeae98f819b38636e67a57c4dc45d2f006 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sun, 11 Dec 2022 19:17:36 +0100 Subject: [PATCH 32/92] towards watcher registration test --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 12 +- .../FileSystemWatcherCreationTest.cs | 120 ++++++++++++++++++ .../FileWatcherExTests.csproj | 1 + 3 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index cbc9cb6..7486b54 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -26,6 +26,8 @@ internal Func GetDirectoryInfosFunc set => _getDirectoryInfosFunc = value; } + internal Dictionary FwDictionary => _fwDictionary; + /// /// Create new instance of FileSystemWatcherWrapper /// @@ -155,12 +157,10 @@ private void MakeWatcher(string path) { if (!_fwDictionary.ContainsKey(item.FullName)) { - var fswItem = new FileSystemWatcherWrapper - { - Path = item.FullName, - IncludeSubdirectories = true, - EnableRaisingEvents = true, - }; + var fswItem = _watcherFactory(); + fswItem.Path = item.FullName; + fswItem.IncludeSubdirectories = true; + fswItem.EnableRaisingEvents = true; // Bind internal events to manipulate the possible symbolic links fswItem.Created += new(MakeWatcher_Created); diff --git a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs new file mode 100644 index 0000000..b257758 --- /dev/null +++ b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs @@ -0,0 +1,120 @@ +using FileWatcherEx; +using Moq; +using Xunit; +using Xunit.Abstractions; + +namespace FileWatcherExTests; + +public class FileSystemWatcherCreationTest +{ + private readonly ITestOutputHelper _testOutputHelper; + private readonly FileWatcher _uut; + private readonly List> _mocks; + + public FileSystemWatcherCreationTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + _uut = new FileWatcher(); + _mocks = new List>(); + + } + + [Fact] + public void Root_Watcher_Is_Created() + { + using var dir = new TempDir(); + + _uut.Create( + dir.FullPath, + e => {}, + e => {}, + WatcherFactoryWithMemory); + + AssertContainsWatcherFor(dir.FullPath); + } + + + // creates 2 sub-directories and 2 nested symlinks + // file watchers are registered for the root dir and the symlinks + // for the sub-directories, no extra file watchers are created + // since the normal file watcher already emits events for subdirs + [Fact] + public void Root_And_Subdir_Watcher_Are_Created() + { + using var dir = new TempDir(); + + // {tempdir}/subdir1 + var subdirPath1 = Path.Combine(dir.FullPath, "subdir1"); + Directory.CreateDirectory(subdirPath1); + + // {tempdir}/subdir2 + var subdirPath2 = Path.Combine(dir.FullPath, "subdir2"); + Directory.CreateDirectory(subdirPath2); + + // symlink {tempdir}/sym1 to {tempdir}/subdir1 + var symlinkPath1 = Path.Combine(dir.FullPath, "sym1"); + Directory.CreateSymbolicLink(symlinkPath1, subdirPath1); + + // symlink {tempdir}/sym1/sym2 to {tempdir}/subdir2 + var symlinkPath2 = Path.Combine(dir.FullPath, "sym1", "sym2"); + Directory.CreateSymbolicLink(symlinkPath2, subdirPath2); + + _uut.Create( + dir.FullPath, + e => {}, + e => {}, + WatcherFactoryWithMemory); + + AssertContainsWatcherFor(dir.FullPath); + AssertContainsWatcherFor(symlinkPath1); + AssertContainsWatcherFor(symlinkPath2); + } + + private void AssertContainsWatcherFor(string path) + { + var _ = _uut.FwDictionary[path]; + var foundMocks = ( + from mock in _mocks + where IsMockFor(mock, path) + select mock) + .Count(); + Assert.Equal(1, foundMocks); + } + + private bool IsMockFor(Mock mock, string path) + { + try + { + mock.VerifySet(w => w.Path = path); + return true; + } + catch (MockException) + { + return false; + } + } + + + private class TempDir : IDisposable + { + public string FullPath { get; } + + public TempDir() + { + FullPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(FullPath); + } + + public void Dispose() + { + Directory.Delete(FullPath, true); + } + } + + private IFileSystemWatcherWrapper WatcherFactoryWithMemory() + { + var mock = new Mock(); + _mocks.Add(mock); + return mock.Object; + } +} \ No newline at end of file diff --git a/Source/FileWatcherExTests/FileWatcherExTests.csproj b/Source/FileWatcherExTests/FileWatcherExTests.csproj index 7d07fd8..610739e 100644 --- a/Source/FileWatcherExTests/FileWatcherExTests.csproj +++ b/Source/FileWatcherExTests/FileWatcherExTests.csproj @@ -10,6 +10,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive From 98355c4bbcb40d44ac571e3c954f6400eddf7b97 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sun, 11 Dec 2022 20:55:32 +0100 Subject: [PATCH 33/92] covered filewatcher --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 4 +- .../FileSystemWatcherCreationTest.cs | 61 ++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 7486b54..6614f7b 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -181,7 +181,7 @@ private void MakeWatcher(string path) } - private void MakeWatcher_Created(object sender, FileSystemEventArgs e) + internal void MakeWatcher_Created(object sender, FileSystemEventArgs e) { try { @@ -214,7 +214,7 @@ private void MakeWatcher_Created(object sender, FileSystemEventArgs e) } - private void MakeWatcher_Deleted(object sender, FileSystemEventArgs e) + internal void MakeWatcher_Deleted(object sender, FileSystemEventArgs e) { // If object removed, then I will dispose and remove them from dictionary if (_fwDictionary.ContainsKey(e.FullPath)) diff --git a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs index b257758..10106ea 100644 --- a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs +++ b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs @@ -38,8 +38,11 @@ public void Root_Watcher_Is_Created() // file watchers are registered for the root dir and the symlinks // for the sub-directories, no extra file watchers are created // since the normal file watcher already emits events for subdirs + // + // this handles the initial setup after start. + // for registering sym-link watchers during runtime, MakeWatcher_Created is used. [Fact] - public void Root_And_Subdir_Watcher_Are_Created() + public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() { using var dir = new TempDir(); @@ -70,6 +73,62 @@ public void Root_And_Subdir_Watcher_Are_Created() AssertContainsWatcherFor(symlinkPath2); } + [Fact] + public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() + { + using var dir = new TempDir(); + _uut.Create( + dir.FullPath, + e => {}, + e => {}, + WatcherFactoryWithMemory); + + // create subdir + var subdirPath = Path.Combine(dir.FullPath, "subdir"); + Directory.CreateDirectory(subdirPath); + + // simulate file watcher trigger + _uut.MakeWatcher_Created(null, + new FileSystemEventArgs(WatcherChangeTypes.Created, dir.FullPath, "subdir")); + + // subdir is ignored + Assert.Single(_uut.FwDictionary); + AssertContainsWatcherFor(dir.FullPath); + + // create symlink + var symlinkPath = Path.Combine(dir.FullPath, "sym"); + Directory.CreateSymbolicLink(symlinkPath, subdirPath); + + // simulate file watcher trigger + _uut.MakeWatcher_Created(null, + new FileSystemEventArgs(WatcherChangeTypes.Created, dir.FullPath, "sym")); + + // symlink dir is registered + Assert.Equal(2, _uut.FwDictionary.Count); + AssertContainsWatcherFor(dir.FullPath); + AssertContainsWatcherFor(symlinkPath); + + // remove the symlink again + Directory.Delete(symlinkPath); + + // simulate file watcher trigger + _uut.MakeWatcher_Deleted(null, + new FileSystemEventArgs(WatcherChangeTypes.Deleted, dir.FullPath, "sym")); + + // sym-link file watcher is removed + Assert.Single(_uut.FwDictionary); + AssertContainsWatcherFor(dir.FullPath); + + _uut.Dispose(); + } + + [Fact] + public void MakeWatcher_Create_Exceptions_Are_Silently_Ignored() + { + _uut.MakeWatcher_Created(null, + new FileSystemEventArgs(WatcherChangeTypes.Created, "/not/existing", "foo")); + } + private void AssertContainsWatcherFor(string path) { var _ = _uut.FwDictionary[path]; From 871adaa1343c435a47b9399fd041d8b5fa9044fe Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sun, 11 Dec 2022 21:40:19 +0100 Subject: [PATCH 34/92] bump coverage --- .../FileSystemWatcherCreationTest.cs | 18 +------ .../FileWatcherExIntegrationTest.cs | 50 +++++++++++++++---- Source/FileWatcherExTests/Helper/TempDir.cs | 17 +++++++ .../ReplayFileSystemWatcherWrapper.cs | 6 ++- 4 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 Source/FileWatcherExTests/Helper/TempDir.cs diff --git a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs index 10106ea..ab5e853 100644 --- a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs +++ b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs @@ -1,4 +1,5 @@ using FileWatcherEx; +using FileWatcherExTests.Helper; using Moq; using Xunit; using Xunit.Abstractions; @@ -153,23 +154,6 @@ private bool IsMockFor(Mock mock, string path) } } - - private class TempDir : IDisposable - { - public string FullPath { get; } - - public TempDir() - { - FullPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); - Directory.CreateDirectory(FullPath); - } - - public void Dispose() - { - Directory.Delete(FullPath, true); - } - } - private IFileSystemWatcherWrapper WatcherFactoryWithMemory() { var mock = new Mock(); diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index c6f601f..c4cd86d 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -1,5 +1,6 @@ using System.Collections.Concurrent; using FileWatcherEx; +using FileWatcherExTests.Helper; using Xunit; namespace FileWatcherExTests; @@ -252,19 +253,48 @@ public void CreateSubDirectoryAddAndRemoveFileWithSleep() Assert.Equal(@"", ev4.OldFullPath); } - // TODO This scenario tries to test the code paths checking for a "reparse point" - // which is a symbolic link in NTFS: https://learn.microsoft.com/en-us/windows/win32/fileio/reparse-points - // Currently, the test setup does not support this. Namely, calls to new DirectoryInfo(path).GetDirectories() and - // File.GetAttributes(...) would need to be wrapped and passed in e.g. as a Func - [Fact (Skip = "test setup needs to be extended")] - public void CreateFileInsideSymbolicLinkDirectory() + [Fact] + public void Filter_Settings_Are_Delegated() { - _fileWatcher.Start(); - _replayer.Replay(@"scenario\create_file_inside_symbolic_link_directory.csv"); - _fileWatcher.Stop(); + using var dir = new TempDir(); + var watcher = new ReplayFileSystemWatcherWrapper(); + + var uut = new FileSystemWatcherEx(dir.FullPath); + uut.FileSystemWatcherFactory = () => watcher; + uut.Filters.Add("*.foo"); + uut.Filters.Add("*.bar"); + + uut.Start(); + Assert.Equal(new List{"*.foo", "*.bar"}, watcher.Filters); + } + + [Fact] + public void Set_Filter() + { + using var dir = new TempDir(); + var watcher = new ReplayFileSystemWatcherWrapper(); + + var uut = new FileSystemWatcherEx(dir.FullPath); + uut.FileSystemWatcherFactory = () => watcher; + + // "all files" by default + Assert.Equal("*", uut.Filter); - Assert.Equal(6, _events.Count); + uut.Filters.Add("*.foo"); + uut.Filters.Add("*.bar"); + + // two filter entries + Assert.Equal(2, uut.Filters.Count); + + // if multiple filters, only first is displayed. TODO Why ? + Assert.Equal("*.foo", uut.Filter); + + uut.Filter = "*.baz"; + Assert.Equal("*.baz", uut.Filter); + Assert.Single(uut.Filters); } + + [Fact(Skip = "requires real (Windows) file system")] public void SimpleRealFileSystemTest() diff --git a/Source/FileWatcherExTests/Helper/TempDir.cs b/Source/FileWatcherExTests/Helper/TempDir.cs new file mode 100644 index 0000000..4c445f1 --- /dev/null +++ b/Source/FileWatcherExTests/Helper/TempDir.cs @@ -0,0 +1,17 @@ +namespace FileWatcherExTests.Helper; + +public class TempDir : IDisposable +{ + public string FullPath { get; } + + public TempDir() + { + FullPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(FullPath); + } + + public void Dispose() + { + Directory.Delete(FullPath, true); + } +} diff --git a/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs b/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs index 6156b14..653eba9 100644 --- a/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs +++ b/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs @@ -21,6 +21,8 @@ double DiffInMilliseconds /// public class ReplayFileSystemWatcherWrapper : IFileSystemWatcherWrapper { + private Collection _filters = new(); + public void Replay(string csvFile) { using var reader = new StreamReader(csvFile); @@ -72,7 +74,9 @@ public void Replay(string csvFile) // unused in replay implementation public string Path { get; set; } - public Collection Filters { get; } + + public Collection Filters => _filters; + public bool IncludeSubdirectories { get; set; } public bool EnableRaisingEvents { get; set; } public NotifyFilters NotifyFilter { get; set; } From ba5c485f2a85c897013267bc94716181ae22b384 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sun, 11 Dec 2022 22:20:09 +0100 Subject: [PATCH 35/92] cleanup --- Source/FileWatcherExTests/EventProcessorTest.cs | 2 +- .../FileWatcherExIntegrationTest.cs | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Source/FileWatcherExTests/EventProcessorTest.cs b/Source/FileWatcherExTests/EventProcessorTest.cs index 73999dc..972e32a 100644 --- a/Source/FileWatcherExTests/EventProcessorTest.cs +++ b/Source/FileWatcherExTests/EventProcessorTest.cs @@ -252,7 +252,7 @@ public void Filter_Out_Deleted_Event_With_Subdirectory() } [Fact] - public void IsParent() + public void Is_Parent() { Assert.True(EventProcessor.IsParent(@"c:\a\b", @"c:")); Assert.True(EventProcessor.IsParent(@"c:\a\b", @"c:\a")); diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index c4cd86d..ce34c89 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -5,11 +5,9 @@ namespace FileWatcherExTests; -// TODO subdirectory tests - /// /// Integration/ Golden master test for FileWatcherEx -/// Considers C:\temp\fwtest to be the test directory +/// Note: the scenarios where recorded in C:\temp\fwtest /// public class FileWatcherExIntegrationTest : IDisposable { @@ -151,7 +149,7 @@ public void Create_Rename_And_Remove_Single_File_With_Wait_Time_Via_WSL2() [Fact] - public void ManuallyCreateAndRenameFileViaWindowsExplorer() + public void Manually_Create_And_Rename_File_Via_Windows_Explorer() { StartFileWatcherAndReplay(@"scenario\create_and_rename_file_via_explorer.csv"); @@ -169,7 +167,7 @@ public void ManuallyCreateAndRenameFileViaWindowsExplorer() } [Fact] - public void ManuallyCreateRenameAndDeleteFileViaWindowsExplorer() + public void Manually_Create_Rename_And_Delete_File_Via_Windows_Explorer() { StartFileWatcherAndReplay(@"scenario\create_rename_and_delete_file_via_explorer.csv"); @@ -191,7 +189,7 @@ public void ManuallyCreateRenameAndDeleteFileViaWindowsExplorer() } [Fact] - public void DownloadImageViaEdgeBrowser() + public void Download_Image_Via_Edge_Browser() { StartFileWatcherAndReplay(@"scenario\download_image_via_Edge_browser.csv"); @@ -209,7 +207,7 @@ public void DownloadImageViaEdgeBrowser() // instantly removed file is not in the events list [Fact] - public void CreateSubDirectoryAddAndRemoveFile() + public void Create_Sub_Directory_Add_And_Remove_File() { StartFileWatcherAndReplay(@"scenario\create_subdirectory_add_and_remove_file.csv"); @@ -226,7 +224,7 @@ public void CreateSubDirectoryAddAndRemoveFile() } [Fact] - public void CreateSubDirectoryAddAndRemoveFileWithSleep() + public void Create_Sub_Directory_Add_And_Remove_File_With_Sleep() { StartFileWatcherAndReplay(@"scenario\create_subdirectory_add_and_remove_file_with_sleep.csv"); @@ -297,7 +295,7 @@ public void Set_Filter() [Fact(Skip = "requires real (Windows) file system")] - public void SimpleRealFileSystemTest() + public void Simple_Real_File_System_Test() { ConcurrentQueue events = new(); var fw = new FileSystemWatcherEx(@"c:\temp\fwtest\"); From 1a9f626b80a8a2a88e5cd4f4fe7a11884a41e882 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Sun, 11 Dec 2022 22:45:18 +0100 Subject: [PATCH 36/92] cleanup --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 9 ++++----- Source/FileWatcherEx/Helpers/FileWatcher.cs | 17 +++++++++-------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 12d69e7..2b820fc 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -10,8 +10,6 @@ namespace FileWatcherEx; /// public class FileSystemWatcherEx : IDisposable { - private readonly Func? _watcherFactory; - #region Private Properties private Thread? _thread; @@ -25,13 +23,14 @@ public class FileSystemWatcherEx : IDisposable // Define the cancellation token. private CancellationTokenSource? _cancelSource; - // how a file watcher is injected + // allow injection of FileSystemWatcherWrapper internal Func FileSystemWatcherFactory { - get => _fswFactory ?? (() => new FileSystemWatcherWrapper()); + // default to production FileSystemWatcherWrapper (which wrapped the native FileSystemWatcher) + get { return _fswFactory ?? (() => new FileSystemWatcherWrapper()); } set => _fswFactory = value; } - + #endregion diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 6614f7b..663ca8f 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -16,13 +16,17 @@ internal class FileWatcher : IDisposable internal Func GetFileAttributesFunc { - get => _getFileAttributesFunc ?? (p => File.GetAttributes(p)); + get => _getFileAttributesFunc ?? File.GetAttributes; set => _getFileAttributesFunc = value; } internal Func GetDirectoryInfosFunc { - get => _getDirectoryInfosFunc ?? (p => new DirectoryInfo(p).GetDirectories()); + get + { + DirectoryInfo[] DefaultFunc(string p) => new DirectoryInfo(p).GetDirectories(); + return _getDirectoryInfosFunc ?? DefaultFunc; + } set => _getDirectoryInfosFunc = value; } @@ -135,12 +139,9 @@ private void MakeWatcher(string path) fileSystemWatcherRoot.Created += new(MakeWatcher_Created); fileSystemWatcherRoot.Deleted += new(MakeWatcher_Deleted); - fileSystemWatcherRoot.Changed += - new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CHANGED)); - fileSystemWatcherRoot.Created += - new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CREATED)); - fileSystemWatcherRoot.Deleted += - new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.DELETED)); + fileSystemWatcherRoot.Changed += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CHANGED)); + fileSystemWatcherRoot.Created += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CREATED)); + fileSystemWatcherRoot.Deleted += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.DELETED)); fileSystemWatcherRoot.Renamed += new((object _, RenamedEventArgs e) => ProcessEvent(e)); fileSystemWatcherRoot.Error += new((object _, ErrorEventArgs e) => _onError?.Invoke(e)); From bb0494bd232cc36389f62f3cdeaaefdb5d072dd5 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 19 Dec 2022 21:01:32 +0100 Subject: [PATCH 37/92] clear IDE warnings + recommendations --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 1 + .../FileWatcherEx/Helpers/EventProcessor.cs | 77 +++++++++---------- .../FileWatcherExTests/EventProcessorTest.cs | 1 + 3 files changed, 39 insertions(+), 40 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 67c2be1..c15d489 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -1,6 +1,7 @@  using System.Collections.Concurrent; using System.ComponentModel; +using FileWatcherEx.Helpers; namespace FileWatcherEx; diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index 2836f7a..df9458e 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -2,34 +2,33 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -namespace FileWatcherEx; +namespace FileWatcherEx.Helpers; internal class EventProcessor { - /// /// Aggregate and only emit events when changes have stopped for this duration (in ms) /// - private static readonly int EVENT_DELAY = 50; + private const int EventDelay = 50; /// /// Warn after certain time span of event spam (in ticks) /// - private static readonly int EVENT_SPAM_WARNING_THRESHOLD = 60 * 1000 * 10000; + private const int EventSpamWarningThreshold = 60 * 1000 * 10000; - private readonly System.Object LOCK = new(); - private Task? delayTask = null; + private readonly object _lock = new(); + private Task? _delayTask = null; - private readonly List events = new(); - private readonly Action handleEvent; + private readonly List _events = new(); + private readonly Action _handleEvent; - private readonly Action logger; + private readonly Action _logger; - private long lastEventTime = 0; - private long delayStarted = 0; + private long _lastEventTime = 0; + private long _delayStarted = 0; - private long spamCheckStartTime = 0; - private bool spamWarningLogged = false; + private long _spamCheckStartTime = 0; + private bool _spamWarningLogged = false; internal static IEnumerable NormalizeEvents(FileChangedEvent[] events) @@ -156,77 +155,75 @@ internal static bool IsParent(FileChangedEvent e, List deletedPaths) internal static bool IsParent(string p, string candidate) { - return p.IndexOf(candidate + '\\') == 0; + return p.IndexOf(candidate + '\\', StringComparison.Ordinal) == 0; } - - - + public EventProcessor(Action onEvent, Action onLogging) { - handleEvent = onEvent; - logger = onLogging; + _handleEvent = onEvent; + _logger = onLogging; } public void ProcessEvent(FileChangedEvent fileEvent) { - lock (LOCK) + lock (_lock) { var now = DateTime.Now.Ticks; // Check for spam - if (events.Count == 0) + if (_events.Count == 0) { - spamWarningLogged = false; - spamCheckStartTime = now; + _spamWarningLogged = false; + _spamCheckStartTime = now; } - else if (!spamWarningLogged && spamCheckStartTime + EVENT_SPAM_WARNING_THRESHOLD < now) + else if (!_spamWarningLogged && _spamCheckStartTime + EventSpamWarningThreshold < now) { - spamWarningLogged = true; - logger(string.Format("Warning: Watcher is busy catching up with {0} file changes in 60 seconds. Latest path is '{1}'", events.Count, fileEvent.FullPath)); + _spamWarningLogged = true; + _logger(string.Format("Warning: Watcher is busy catching up with {0} file changes in 60 seconds. Latest path is '{1}'", _events.Count, fileEvent.FullPath)); } // Add into our queue - events.Add(fileEvent); - lastEventTime = now; + _events.Add(fileEvent); + _lastEventTime = now; // Process queue after delay - if (delayTask == null) + if (_delayTask == null) { // Create function to buffer events - void func(Task value) + void Func(Task value) { - lock (LOCK) + lock (_lock) { // Check if another event has been received in the meantime - if (delayStarted == lastEventTime) + if (_delayStarted == _lastEventTime) { // Normalize and handle - var normalized = NormalizeEvents(events.ToArray()); + var normalized = NormalizeEvents(_events.ToArray()); foreach (var e in normalized) { - handleEvent(e); + _handleEvent(e); } // Reset - events.Clear(); - delayTask = null; + _events.Clear(); + _delayTask = null; } // Otherwise we have received a new event while this task was // delayed and we reschedule it. else { - delayStarted = lastEventTime; - delayTask = Task.Delay(EVENT_DELAY).ContinueWith(func); + _delayStarted = _lastEventTime; + _delayTask = Task.Delay(EventDelay).ContinueWith(Func); } } } // Start function after delay - delayStarted = lastEventTime; - delayTask = Task.Delay(EVENT_DELAY).ContinueWith(func); + _delayStarted = _lastEventTime; + _delayTask = Task.Delay(EventDelay).ContinueWith(Func); } } } diff --git a/Source/FileWatcherExTests/EventProcessorTest.cs b/Source/FileWatcherExTests/EventProcessorTest.cs index 972e32a..0806a4d 100644 --- a/Source/FileWatcherExTests/EventProcessorTest.cs +++ b/Source/FileWatcherExTests/EventProcessorTest.cs @@ -1,5 +1,6 @@ using Xunit; using FileWatcherEx; +using FileWatcherEx.Helpers; namespace FileWatcherExTests; From 12a8aab5e485787d35aa67ab9b542bf2459a0a69 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 19 Dec 2022 21:11:37 +0100 Subject: [PATCH 38/92] simplify --- .../FileWatcherEx/Helpers/EventProcessor.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index df9458e..220f290 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -2,6 +2,8 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +using static FileWatcherEx.ChangeType; + namespace FileWatcherEx.Helpers; internal class EventProcessor @@ -39,48 +41,48 @@ internal static IEnumerable NormalizeEvents(FileChangedEvent[] // Normalize duplicates foreach (var newEvent in events) { - mapPathToEvents.TryGetValue(newEvent.FullPath, out var oldEvent); // Try get event from newEvent.FullPath + mapPathToEvents.TryGetValue(newEvent.FullPath, out FileChangedEvent? oldEvent); // Try get event from newEvent.FullPath - if (oldEvent != null && oldEvent.ChangeType == ChangeType.CREATED && newEvent.ChangeType == ChangeType.DELETED) + if (oldEvent?.ChangeType == CREATED && newEvent.ChangeType == DELETED) { // CREATED + DELETED => remove mapPathToEvents.Remove(oldEvent.FullPath); eventsWithoutDuplicates.Remove(oldEvent); } else - if (oldEvent != null && oldEvent.ChangeType == ChangeType.DELETED && newEvent.ChangeType == ChangeType.CREATED) + if (oldEvent?.ChangeType == DELETED && newEvent.ChangeType == CREATED) { // DELETED + CREATED => CHANGED - oldEvent.ChangeType = ChangeType.CHANGED; + oldEvent.ChangeType = CHANGED; } else - if (oldEvent != null && oldEvent.ChangeType == ChangeType.CREATED && newEvent.ChangeType == ChangeType.CHANGED) + if (oldEvent?.ChangeType == CREATED && newEvent.ChangeType == CHANGED) { // CREATED + CHANGED => CREATED // Do nothing } else { // Otherwise - if (newEvent.ChangeType == ChangeType.RENAMED) + if (newEvent.ChangeType == RENAMED) { // If + RENAMED do { mapPathToEvents.TryGetValue(newEvent.OldFullPath!, out var renameFromEvent); // Try get event from newEvent.OldFullPath - if (renameFromEvent != null && renameFromEvent.ChangeType == ChangeType.CREATED) + if (renameFromEvent != null && renameFromEvent.ChangeType == CREATED) { // If rename from CREATED file // Remove data about the CREATED file mapPathToEvents.Remove(renameFromEvent.FullPath); eventsWithoutDuplicates.Remove(renameFromEvent); // Handle new event as CREATED - newEvent.ChangeType = ChangeType.CREATED; + newEvent.ChangeType = CREATED; newEvent.OldFullPath = null; - if (oldEvent != null && oldEvent.ChangeType == ChangeType.DELETED) + if (oldEvent?.ChangeType == DELETED) { // DELETED + CREATED => CHANGED - newEvent.ChangeType = ChangeType.CHANGED; + newEvent.ChangeType = CHANGED; } } else - if (renameFromEvent != null && renameFromEvent.ChangeType == ChangeType.RENAMED) + if (renameFromEvent != null && renameFromEvent.ChangeType == RENAMED) { // If rename from RENAMED file // Remove data about the RENAMED file mapPathToEvents.Remove(renameFromEvent.FullPath); @@ -138,7 +140,7 @@ internal static IEnumerable FilterDeleted(IEnumerable deletedPaths) { - if (e.ChangeType == ChangeType.DELETED) + if (e.ChangeType == DELETED) { if (deletedPaths.Any(d => IsParent(e.FullPath, d))) { From 0e597eb9d9219cb2d476c9e5b49949fd889ec878 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 19 Dec 2022 21:13:19 +0100 Subject: [PATCH 39/92] format --- .../FileWatcherEx/Helpers/EventProcessor.cs | 77 +++++++++++-------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index 220f290..b020c21 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -41,35 +41,41 @@ internal static IEnumerable NormalizeEvents(FileChangedEvent[] // Normalize duplicates foreach (var newEvent in events) { - mapPathToEvents.TryGetValue(newEvent.FullPath, out FileChangedEvent? oldEvent); // Try get event from newEvent.FullPath + mapPathToEvents.TryGetValue(newEvent.FullPath, + out FileChangedEvent? oldEvent); // Try get event from newEvent.FullPath if (oldEvent?.ChangeType == CREATED && newEvent.ChangeType == DELETED) - { // CREATED + DELETED => remove + { + // CREATED + DELETED => remove mapPathToEvents.Remove(oldEvent.FullPath); eventsWithoutDuplicates.Remove(oldEvent); } - else - if (oldEvent?.ChangeType == DELETED && newEvent.ChangeType == CREATED) - { // DELETED + CREATED => CHANGED + else if (oldEvent?.ChangeType == DELETED && newEvent.ChangeType == CREATED) + { + // DELETED + CREATED => CHANGED oldEvent.ChangeType = CHANGED; } - else - if (oldEvent?.ChangeType == CREATED && newEvent.ChangeType == CHANGED) - { // CREATED + CHANGED => CREATED - // Do nothing + else if (oldEvent?.ChangeType == CREATED && newEvent.ChangeType == CHANGED) + { + // CREATED + CHANGED => CREATED + // Do nothing } else - { // Otherwise + { + // Otherwise if (newEvent.ChangeType == RENAMED) - { // If + RENAMED + { + // If + RENAMED do { - mapPathToEvents.TryGetValue(newEvent.OldFullPath!, out var renameFromEvent); // Try get event from newEvent.OldFullPath + mapPathToEvents.TryGetValue(newEvent.OldFullPath!, + out var renameFromEvent); // Try get event from newEvent.OldFullPath if (renameFromEvent != null && renameFromEvent.ChangeType == CREATED) - { // If rename from CREATED file - // Remove data about the CREATED file + { + // If rename from CREATED file + // Remove data about the CREATED file mapPathToEvents.Remove(renameFromEvent.FullPath); eventsWithoutDuplicates.Remove(renameFromEvent); // Handle new event as CREATED @@ -77,14 +83,15 @@ internal static IEnumerable NormalizeEvents(FileChangedEvent[] newEvent.OldFullPath = null; if (oldEvent?.ChangeType == DELETED) - { // DELETED + CREATED => CHANGED + { + // DELETED + CREATED => CHANGED newEvent.ChangeType = CHANGED; } } - else - if (renameFromEvent != null && renameFromEvent.ChangeType == RENAMED) - { // If rename from RENAMED file - // Remove data about the RENAMED file + else if (renameFromEvent != null && renameFromEvent.ChangeType == RENAMED) + { + // If rename from RENAMED file + // Remove data about the RENAMED file mapPathToEvents.Remove(renameFromEvent.FullPath); eventsWithoutDuplicates.Remove(renameFromEvent); // Change OldFullPath @@ -93,30 +100,33 @@ internal static IEnumerable NormalizeEvents(FileChangedEvent[] continue; } else - { // Otherwise - // Do nothing - //mapPathToEvents.TryGetValue(newEvent.OldFullPath, out oldEvent); // Try get event from newEvent.OldFullPath + { + // Otherwise + // Do nothing + //mapPathToEvents.TryGetValue(newEvent.OldFullPath, out oldEvent); // Try get event from newEvent.OldFullPath } } while (false); } if (oldEvent != null) - { // If old event exists - // Replace old event data with data from the new event + { + // If old event exists + // Replace old event data with data from the new event oldEvent.ChangeType = newEvent.ChangeType; oldEvent.OldFullPath = newEvent.OldFullPath; } else - { // If old event is not exist - // Add new event + { + // If old event is not exist + // Add new event mapPathToEvents.Add(newEvent.FullPath, newEvent); eventsWithoutDuplicates.Add(newEvent); } } } - - return FilterDeleted(eventsWithoutDuplicates); + + return FilterDeleted(eventsWithoutDuplicates); } // This algorithm will remove all DELETE events up to the root folder @@ -159,7 +169,7 @@ internal static bool IsParent(string p, string candidate) { return p.IndexOf(candidate + '\\', StringComparison.Ordinal) == 0; } - + public EventProcessor(Action onEvent, Action onLogging) { @@ -183,7 +193,9 @@ public void ProcessEvent(FileChangedEvent fileEvent) else if (!_spamWarningLogged && _spamCheckStartTime + EventSpamWarningThreshold < now) { _spamWarningLogged = true; - _logger(string.Format("Warning: Watcher is busy catching up with {0} file changes in 60 seconds. Latest path is '{1}'", _events.Count, fileEvent.FullPath)); + _logger(string.Format( + "Warning: Watcher is busy catching up with {0} file changes in 60 seconds. Latest path is '{1}'", + _events.Count, fileEvent.FullPath)); } // Add into our queue @@ -229,7 +241,4 @@ void Func(Task value) } } } - - -} - +} \ No newline at end of file From db21785500d61e6a0fee30091e92e346524e90a1 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 19 Dec 2022 21:29:27 +0100 Subject: [PATCH 40/92] small refactorings --- .../FileWatcherEx/Helpers/EventProcessor.cs | 76 ++++++++----------- 1 file changed, 32 insertions(+), 44 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index b020c21..bb96e8d 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -67,61 +67,49 @@ internal static IEnumerable NormalizeEvents(FileChangedEvent[] if (newEvent.ChangeType == RENAMED) { // If + RENAMED - do - { - mapPathToEvents.TryGetValue(newEvent.OldFullPath!, - out var renameFromEvent); // Try get event from newEvent.OldFullPath + mapPathToEvents.TryGetValue(newEvent.OldFullPath!, + out var renameFromEvent); // Try get event from newEvent.OldFullPath - if (renameFromEvent != null && renameFromEvent.ChangeType == CREATED) - { - // If rename from CREATED file - // Remove data about the CREATED file - mapPathToEvents.Remove(renameFromEvent.FullPath); - eventsWithoutDuplicates.Remove(renameFromEvent); - // Handle new event as CREATED - newEvent.ChangeType = CREATED; - newEvent.OldFullPath = null; - - if (oldEvent?.ChangeType == DELETED) - { - // DELETED + CREATED => CHANGED - newEvent.ChangeType = CHANGED; - } - } - else if (renameFromEvent != null && renameFromEvent.ChangeType == RENAMED) - { - // If rename from RENAMED file - // Remove data about the RENAMED file - mapPathToEvents.Remove(renameFromEvent.FullPath); - eventsWithoutDuplicates.Remove(renameFromEvent); - // Change OldFullPath - newEvent.OldFullPath = renameFromEvent.OldFullPath; - // Check again - continue; - } - else + if (renameFromEvent?.ChangeType == CREATED) + { + // If rename from CREATED file + // Remove data about the CREATED file + mapPathToEvents.Remove(renameFromEvent.FullPath); + eventsWithoutDuplicates.Remove(renameFromEvent); + // Handle new event as CREATED + newEvent.ChangeType = CREATED; + newEvent.OldFullPath = null; + + if (oldEvent?.ChangeType == DELETED) { - // Otherwise - // Do nothing - //mapPathToEvents.TryGetValue(newEvent.OldFullPath, out oldEvent); // Try get event from newEvent.OldFullPath + // DELETED + CREATED => CHANGED + newEvent.ChangeType = CHANGED; } - } while (false); + } + else if (renameFromEvent?.ChangeType == RENAMED) + { + // If rename from RENAMED file + // Remove data about the RENAMED file + mapPathToEvents.Remove(renameFromEvent.FullPath); + eventsWithoutDuplicates.Remove(renameFromEvent); + // Change OldFullPath + newEvent.OldFullPath = renameFromEvent.OldFullPath; + } } - if (oldEvent != null) + if (oldEvent is null) + { + // If old event does not exist, add new event + mapPathToEvents.Add(newEvent.FullPath, newEvent); + eventsWithoutDuplicates.Add(newEvent); + } + else { // If old event exists // Replace old event data with data from the new event oldEvent.ChangeType = newEvent.ChangeType; oldEvent.OldFullPath = newEvent.OldFullPath; } - else - { - // If old event is not exist - // Add new event - mapPathToEvents.Add(newEvent.FullPath, newEvent); - eventsWithoutDuplicates.Add(newEvent); - } } } From 2db6329ab29991c97996d2890e72c2d82c377c06 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 19 Dec 2022 22:02:47 +0100 Subject: [PATCH 41/92] towards extensive switch case --- .../FileWatcherEx/Helpers/EventProcessor.cs | 137 ++++++++++-------- 1 file changed, 73 insertions(+), 64 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index bb96e8d..22b51f1 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -36,85 +36,94 @@ internal class EventProcessor internal static IEnumerable NormalizeEvents(FileChangedEvent[] events) { var mapPathToEvents = new Dictionary(); - var eventsWithoutDuplicates = new List(); - // Normalize duplicates - foreach (var newEvent in events) + FileChangedEvent? FindEvent(string? path) { - mapPathToEvents.TryGetValue(newEvent.FullPath, - out FileChangedEvent? oldEvent); // Try get event from newEvent.FullPath + mapPathToEvents.TryGetValue(path ?? "", out var oldEvent); + return oldEvent; + } - if (oldEvent?.ChangeType == CREATED && newEvent.ChangeType == DELETED) - { - // CREATED + DELETED => remove - mapPathToEvents.Remove(oldEvent.FullPath); - eventsWithoutDuplicates.Remove(oldEvent); - } - else if (oldEvent?.ChangeType == DELETED && newEvent.ChangeType == CREATED) - { - // DELETED + CREATED => CHANGED - oldEvent.ChangeType = CHANGED; - } - else if (oldEvent?.ChangeType == CREATED && newEvent.ChangeType == CHANGED) + + void RemoveEvent(FileChangedEvent ev) + { + mapPathToEvents.Remove(ev.FullPath); + } + + void AddOrUpdate(FileChangedEvent newEvent) + { + if (mapPathToEvents.TryGetValue(newEvent.FullPath, out var oldEvent)) { - // CREATED + CHANGED => CREATED - // Do nothing + // update existing + oldEvent.ChangeType = newEvent.ChangeType; + oldEvent.OldFullPath = newEvent.OldFullPath; } else { - // Otherwise + // add + mapPathToEvents[newEvent.FullPath] = newEvent; + } + } - if (newEvent.ChangeType == RENAMED) - { - // If + RENAMED - mapPathToEvents.TryGetValue(newEvent.OldFullPath!, - out var renameFromEvent); // Try get event from newEvent.OldFullPath + // Normalize duplicates + foreach (var newEvent in events) + { + var oldEvent = FindEvent(newEvent.FullPath); + // original file event from which we renamed + var renameFromEvent = newEvent.ChangeType == RENAMED + ? FindEvent(newEvent.OldFullPath) + : null; - if (renameFromEvent?.ChangeType == CREATED) - { - // If rename from CREATED file - // Remove data about the CREATED file - mapPathToEvents.Remove(renameFromEvent.FullPath); - eventsWithoutDuplicates.Remove(renameFromEvent); - // Handle new event as CREATED - newEvent.ChangeType = CREATED; - newEvent.OldFullPath = null; - - if (oldEvent?.ChangeType == DELETED) - { - // DELETED + CREATED => CHANGED - newEvent.ChangeType = CHANGED; - } - } - else if (renameFromEvent?.ChangeType == RENAMED) + switch (newEvent.ChangeType) + { + // CREATED followed by DELETED => remove + case DELETED when oldEvent?.ChangeType == CREATED: + RemoveEvent(oldEvent); + break; + + // DELETED followed by CREATED => CHANGED + case CREATED when oldEvent?.ChangeType == DELETED: + oldEvent.ChangeType = CHANGED; + break; + + // CREATED followed by CHANGED => CREATED + case CHANGED when oldEvent?.ChangeType == CREATED: + // Do nothing + break; + + // rename from CREATED file + case RENAMED when renameFromEvent?.ChangeType == CREATED: + // Remove data about the CREATED file + RemoveEvent(renameFromEvent); + // Handle new event as CREATED + newEvent.ChangeType = CREATED; + newEvent.OldFullPath = null; + + if (oldEvent?.ChangeType == DELETED) { - // If rename from RENAMED file - // Remove data about the RENAMED file - mapPathToEvents.Remove(renameFromEvent.FullPath); - eventsWithoutDuplicates.Remove(renameFromEvent); - // Change OldFullPath - newEvent.OldFullPath = renameFromEvent.OldFullPath; + // DELETED followed by CREATED => CHANGED + newEvent.ChangeType = CHANGED; } - } - if (oldEvent is null) - { - // If old event does not exist, add new event - mapPathToEvents.Add(newEvent.FullPath, newEvent); - eventsWithoutDuplicates.Add(newEvent); - } - else - { - // If old event exists - // Replace old event data with data from the new event - oldEvent.ChangeType = newEvent.ChangeType; - oldEvent.OldFullPath = newEvent.OldFullPath; - } + AddOrUpdate(newEvent); + break; + + case RENAMED when renameFromEvent?.ChangeType == RENAMED: + // Remove data about the RENAMED file + RemoveEvent(renameFromEvent); + newEvent.OldFullPath = renameFromEvent.OldFullPath; + AddOrUpdate(newEvent); + break; + + // TODO why does "LOG" need to be in the filewevent ? + case LOG: + + default: + AddOrUpdate(newEvent); + break; } } - - return FilterDeleted(eventsWithoutDuplicates); + return FilterDeleted(mapPathToEvents.Values.ToList()); } // This algorithm will remove all DELETE events up to the root folder From 777174f8c17120ded187a9ba697b4409dffa7cc9 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 19 Dec 2022 23:36:15 +0100 Subject: [PATCH 42/92] own repo --- .../FileWatcherEx/Helpers/EventProcessor.cs | 88 ++++++++++--------- 1 file changed, 48 insertions(+), 40 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index 22b51f1..bf2bf7b 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -31,53 +31,25 @@ internal class EventProcessor private long _spamCheckStartTime = 0; private bool _spamWarningLogged = false; - - + internal static IEnumerable NormalizeEvents(FileChangedEvent[] events) { - var mapPathToEvents = new Dictionary(); - - FileChangedEvent? FindEvent(string? path) - { - mapPathToEvents.TryGetValue(path ?? "", out var oldEvent); - return oldEvent; - } - - - void RemoveEvent(FileChangedEvent ev) - { - mapPathToEvents.Remove(ev.FullPath); - } - - void AddOrUpdate(FileChangedEvent newEvent) - { - if (mapPathToEvents.TryGetValue(newEvent.FullPath, out var oldEvent)) - { - // update existing - oldEvent.ChangeType = newEvent.ChangeType; - oldEvent.OldFullPath = newEvent.OldFullPath; - } - else - { - // add - mapPathToEvents[newEvent.FullPath] = newEvent; - } - } + var eventRepo = new FileEventRepository(); // Normalize duplicates foreach (var newEvent in events) { - var oldEvent = FindEvent(newEvent.FullPath); + var oldEvent = eventRepo.Find(newEvent.FullPath); // original file event from which we renamed var renameFromEvent = newEvent.ChangeType == RENAMED - ? FindEvent(newEvent.OldFullPath) + ? eventRepo.Find(newEvent.OldFullPath) : null; switch (newEvent.ChangeType) { // CREATED followed by DELETED => remove case DELETED when oldEvent?.ChangeType == CREATED: - RemoveEvent(oldEvent); + eventRepo.Remove(oldEvent); break; // DELETED followed by CREATED => CHANGED @@ -93,7 +65,7 @@ void AddOrUpdate(FileChangedEvent newEvent) // rename from CREATED file case RENAMED when renameFromEvent?.ChangeType == CREATED: // Remove data about the CREATED file - RemoveEvent(renameFromEvent); + eventRepo.Remove(renameFromEvent); // Handle new event as CREATED newEvent.ChangeType = CREATED; newEvent.OldFullPath = null; @@ -104,26 +76,62 @@ void AddOrUpdate(FileChangedEvent newEvent) newEvent.ChangeType = CHANGED; } - AddOrUpdate(newEvent); + eventRepo.AddOrUpdate(newEvent); break; case RENAMED when renameFromEvent?.ChangeType == RENAMED: - // Remove data about the RENAMED file - RemoveEvent(renameFromEvent); newEvent.OldFullPath = renameFromEvent.OldFullPath; - AddOrUpdate(newEvent); + // Remove data about the RENAMED file + eventRepo.Remove(renameFromEvent); + eventRepo.AddOrUpdate(newEvent); break; // TODO why does "LOG" need to be in the filewevent ? case LOG: default: - AddOrUpdate(newEvent); + eventRepo.AddOrUpdate(newEvent); break; } } - return FilterDeleted(mapPathToEvents.Values.ToList()); + return FilterDeleted(eventRepo.Events()); + } + + private class FileEventRepository + { + private readonly Dictionary _mapPathToEvents = new(); + + public void AddOrUpdate(FileChangedEvent newEvent) + { + if (_mapPathToEvents.TryGetValue(newEvent.FullPath, out var oldEvent)) + { + // update existing + oldEvent.ChangeType = newEvent.ChangeType; + oldEvent.OldFullPath = newEvent.OldFullPath; + } + else + { + // add + _mapPathToEvents[newEvent.FullPath] = newEvent; + } + } + + public void Remove(FileChangedEvent ev) + { + _mapPathToEvents.Remove(ev.FullPath); + } + + public FileChangedEvent? Find(string? path) + { + _mapPathToEvents.TryGetValue(path ?? "", out var oldEvent); + return oldEvent; + } + + public List Events() + { + return _mapPathToEvents.Values.ToList(); + } } // This algorithm will remove all DELETE events up to the root folder From 2ed45230423c2d99c339456153afb48999624b2a Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Mon, 19 Dec 2022 23:56:48 +0100 Subject: [PATCH 43/92] extract class --- .../FileWatcherEx/Helpers/EventNormalizer.cs | 169 ++++++++++++++++++ .../FileWatcherEx/Helpers/EventProcessor.cs | 148 +-------------- ...rocessorTest.cs => EventNormalizerTest.cs} | 26 +-- 3 files changed, 183 insertions(+), 160 deletions(-) create mode 100644 Source/FileWatcherEx/Helpers/EventNormalizer.cs rename Source/FileWatcherExTests/{EventProcessorTest.cs => EventNormalizerTest.cs} (90%) diff --git a/Source/FileWatcherEx/Helpers/EventNormalizer.cs b/Source/FileWatcherEx/Helpers/EventNormalizer.cs new file mode 100644 index 0000000..88b3733 --- /dev/null +++ b/Source/FileWatcherEx/Helpers/EventNormalizer.cs @@ -0,0 +1,169 @@ +using static FileWatcherEx.ChangeType; + +namespace FileWatcherEx.Helpers; + +/// +/// Tries to fix the real life oddities of the underlying FileSystemWatcher class. +/// The code here got refactored from the original Microsoft sources. +/// For real scenario, see EventNormalizerTest.cs +/// +internal class EventNormalizer +{ + private readonly FileEventRepository _eventRepo = new(); + + internal IEnumerable Normalize(FileChangedEvent[] events) + { + NormalizeDuplicates(events); + return FilterDeleted(_eventRepo.Events()); + } + + private void NormalizeDuplicates(FileChangedEvent[] events) + { + foreach (var newEvent in events) + { + var oldEvent = _eventRepo.Find(newEvent.FullPath); + // original file event from which we renamed, only applicable for RENAMED event + var renameFromEvent = newEvent.ChangeType == RENAMED + ? _eventRepo.Find(newEvent.OldFullPath) + : null; + + switch (newEvent.ChangeType) + { + // CREATED followed by CHANGED => CREATED + case CHANGED when oldEvent?.ChangeType == CREATED: + // Do nothing + break; + + // CREATED followed by DELETED => remove + case DELETED when oldEvent?.ChangeType == CREATED: + _eventRepo.Remove(oldEvent); + break; + + // DELETED followed by CREATED => CHANGED + case CREATED when oldEvent?.ChangeType == DELETED: + oldEvent.ChangeType = CHANGED; + break; + + // Scenario: + // - file foo is created + // - file bar is deleted + // - now foo is renamed to the just deleted bar + // - this results into a bar changed event + case RENAMED when oldEvent?.ChangeType == DELETED && renameFromEvent?.ChangeType == CREATED: + newEvent.ChangeType = CHANGED; + newEvent.OldFullPath = null; + _eventRepo.AddOrUpdate(newEvent); + + // Remove data about the CREATED file + _eventRepo.Remove(renameFromEvent); + break; + + // rename from CREATED file, all other cases + case RENAMED when renameFromEvent?.ChangeType == CREATED: + newEvent.ChangeType = CREATED; + newEvent.OldFullPath = null; + _eventRepo.AddOrUpdate(newEvent); + + // Remove data about the CREATED file + _eventRepo.Remove(renameFromEvent); + break; + + case RENAMED when renameFromEvent?.ChangeType == RENAMED: + newEvent.OldFullPath = renameFromEvent.OldFullPath; + _eventRepo.AddOrUpdate(newEvent); + + // Remove data about the RENAMED file + _eventRepo.Remove(renameFromEvent); + break; + + // the LOG event is not coming from the filesystem, hence it is ignored. + // ideally, LOG would disappear completely but unfortunately it is part of the public API of this lib + case LOG: + // ignore + break; + + default: + _eventRepo.AddOrUpdate(newEvent); + break; + } + } + } + + // This algorithm will remove all DELETE events up to the root folder + // that got deleted if any. This ensures that we are not producing + // DELETE events for each file inside a folder that gets deleted. + // + // 1.) split ADD/CHANGE and DELETED events + // 2.) sort short deleted paths to the top + // 3.) for each DELETE, check if there is a deleted parent and ignore the event in that case + internal static IEnumerable FilterDeleted(IEnumerable eventsWithoutDuplicates) + { + // Handle deletes + var deletedPaths = new List(); + return eventsWithoutDuplicates + .Select((e, n) => new KeyValuePair(n, e)) // store original position value + .OrderBy(e => e.Value.FullPath.Length) // shortest path first + .Where(e => IsParent(e.Value, deletedPaths)) + .OrderBy(e => e.Key) // restore original position + .Select(e => e.Value); + } + + internal static bool IsParent(FileChangedEvent e, List deletedPaths) + { + if (e.ChangeType == DELETED) + { + if (deletedPaths.Any(d => IsParent(e.FullPath, d))) + { + return false; // DELETE is ignored if parent is deleted already + } + + // otherwise mark as deleted + deletedPaths.Add(e.FullPath); + } + + return true; + } + + + internal static bool IsParent(string p, string candidate) + { + return p.IndexOf(candidate + '\\', StringComparison.Ordinal) == 0; + } + + + private class FileEventRepository + { + private readonly Dictionary _mapPathToEvents = new(); + + public void AddOrUpdate(FileChangedEvent newEvent) + { + if (_mapPathToEvents.TryGetValue(newEvent.FullPath, out var oldEvent)) + { + // update existing + oldEvent.ChangeType = newEvent.ChangeType; + oldEvent.OldFullPath = newEvent.OldFullPath; + } + else + { + // add + _mapPathToEvents[newEvent.FullPath] = newEvent; + } + } + + public void Remove(FileChangedEvent ev) + { + _mapPathToEvents.Remove(ev.FullPath); + } + + public FileChangedEvent? Find(string? path) + { + _mapPathToEvents.TryGetValue(path ?? "", out var oldEvent); + return oldEvent; + } + + public List Events() + { + return _mapPathToEvents.Values.ToList(); + } + } +} diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index bf2bf7b..f99977a 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -2,8 +2,6 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -using static FileWatcherEx.ChangeType; - namespace FileWatcherEx.Helpers; internal class EventProcessor @@ -32,150 +30,6 @@ internal class EventProcessor private long _spamCheckStartTime = 0; private bool _spamWarningLogged = false; - internal static IEnumerable NormalizeEvents(FileChangedEvent[] events) - { - var eventRepo = new FileEventRepository(); - - // Normalize duplicates - foreach (var newEvent in events) - { - var oldEvent = eventRepo.Find(newEvent.FullPath); - // original file event from which we renamed - var renameFromEvent = newEvent.ChangeType == RENAMED - ? eventRepo.Find(newEvent.OldFullPath) - : null; - - switch (newEvent.ChangeType) - { - // CREATED followed by DELETED => remove - case DELETED when oldEvent?.ChangeType == CREATED: - eventRepo.Remove(oldEvent); - break; - - // DELETED followed by CREATED => CHANGED - case CREATED when oldEvent?.ChangeType == DELETED: - oldEvent.ChangeType = CHANGED; - break; - - // CREATED followed by CHANGED => CREATED - case CHANGED when oldEvent?.ChangeType == CREATED: - // Do nothing - break; - - // rename from CREATED file - case RENAMED when renameFromEvent?.ChangeType == CREATED: - // Remove data about the CREATED file - eventRepo.Remove(renameFromEvent); - // Handle new event as CREATED - newEvent.ChangeType = CREATED; - newEvent.OldFullPath = null; - - if (oldEvent?.ChangeType == DELETED) - { - // DELETED followed by CREATED => CHANGED - newEvent.ChangeType = CHANGED; - } - - eventRepo.AddOrUpdate(newEvent); - break; - - case RENAMED when renameFromEvent?.ChangeType == RENAMED: - newEvent.OldFullPath = renameFromEvent.OldFullPath; - // Remove data about the RENAMED file - eventRepo.Remove(renameFromEvent); - eventRepo.AddOrUpdate(newEvent); - break; - - // TODO why does "LOG" need to be in the filewevent ? - case LOG: - - default: - eventRepo.AddOrUpdate(newEvent); - break; - } - } - - return FilterDeleted(eventRepo.Events()); - } - - private class FileEventRepository - { - private readonly Dictionary _mapPathToEvents = new(); - - public void AddOrUpdate(FileChangedEvent newEvent) - { - if (_mapPathToEvents.TryGetValue(newEvent.FullPath, out var oldEvent)) - { - // update existing - oldEvent.ChangeType = newEvent.ChangeType; - oldEvent.OldFullPath = newEvent.OldFullPath; - } - else - { - // add - _mapPathToEvents[newEvent.FullPath] = newEvent; - } - } - - public void Remove(FileChangedEvent ev) - { - _mapPathToEvents.Remove(ev.FullPath); - } - - public FileChangedEvent? Find(string? path) - { - _mapPathToEvents.TryGetValue(path ?? "", out var oldEvent); - return oldEvent; - } - - public List Events() - { - return _mapPathToEvents.Values.ToList(); - } - } - - // This algorithm will remove all DELETE events up to the root folder - // that got deleted if any. This ensures that we are not producing - // DELETE events for each file inside a folder that gets deleted. - // - // 1.) split ADD/CHANGE and DELETED events - // 2.) sort short deleted paths to the top - // 3.) for each DELETE, check if there is a deleted parent and ignore the event in that case - internal static IEnumerable FilterDeleted(IEnumerable eventsWithoutDuplicates) - { - // Handle deletes - var deletedPaths = new List(); - return eventsWithoutDuplicates - .Select((e, n) => new KeyValuePair(n, e)) // store original position value - .OrderBy(e => e.Value.FullPath.Length) // shortest path first - .Where(e => IsParent(e.Value, deletedPaths)) - .OrderBy(e => e.Key) // restore original position - .Select(e => e.Value); - } - - internal static bool IsParent(FileChangedEvent e, List deletedPaths) - { - if (e.ChangeType == DELETED) - { - if (deletedPaths.Any(d => IsParent(e.FullPath, d))) - { - return false; // DELETE is ignored if parent is deleted already - } - - // otherwise mark as deleted - deletedPaths.Add(e.FullPath); - } - - return true; - } - - - internal static bool IsParent(string p, string candidate) - { - return p.IndexOf(candidate + '\\', StringComparison.Ordinal) == 0; - } - - public EventProcessor(Action onEvent, Action onLogging) { _handleEvent = onEvent; @@ -219,7 +73,7 @@ void Func(Task value) if (_delayStarted == _lastEventTime) { // Normalize and handle - var normalized = NormalizeEvents(_events.ToArray()); + var normalized = new EventNormalizer().Normalize(_events.ToArray()); foreach (var e in normalized) { _handleEvent(e); diff --git a/Source/FileWatcherExTests/EventProcessorTest.cs b/Source/FileWatcherExTests/EventNormalizerTest.cs similarity index 90% rename from Source/FileWatcherExTests/EventProcessorTest.cs rename to Source/FileWatcherExTests/EventNormalizerTest.cs index 0806a4d..1cf5bb1 100644 --- a/Source/FileWatcherExTests/EventProcessorTest.cs +++ b/Source/FileWatcherExTests/EventNormalizerTest.cs @@ -4,7 +4,7 @@ namespace FileWatcherExTests; -public class EventProcessorTest +public class EventNormalizerTest { [Fact] public void No_Input_Gives_No_Output() @@ -215,7 +215,7 @@ public void Filter_Passes_Events_Through() } }; - var filtered = EventProcessor.FilterDeleted(events); + var filtered = EventNormalizer.FilterDeleted(events); Assert.Equal(events, filtered); } @@ -244,7 +244,7 @@ public void Filter_Out_Deleted_Event_With_Subdirectory() } }; - var filtered = EventProcessor.FilterDeleted(events).ToList(); + var filtered = EventNormalizer.FilterDeleted(events).ToList(); Assert.Equal(2, filtered.Count); Assert.Equal(ChangeType.DELETED, filtered[0].ChangeType); Assert.Equal(@"c:\bar", filtered[0].FullPath); @@ -255,15 +255,15 @@ public void Filter_Out_Deleted_Event_With_Subdirectory() [Fact] public void Is_Parent() { - Assert.True(EventProcessor.IsParent(@"c:\a\b", @"c:")); - Assert.True(EventProcessor.IsParent(@"c:\a\b", @"c:\a")); + Assert.True(EventNormalizer.IsParent(@"c:\a\b", @"c:")); + Assert.True(EventNormalizer.IsParent(@"c:\a\b", @"c:\a")); // candidate must not have backslash - Assert.False(EventProcessor.IsParent(@"c:\a\b", @"c:\")); - Assert.False(EventProcessor.IsParent(@"c:\a\b", @"c:\a\")); + Assert.False(EventNormalizer.IsParent(@"c:\a\b", @"c:\")); + Assert.False(EventNormalizer.IsParent(@"c:\a\b", @"c:\a\")); - Assert.False(EventProcessor.IsParent(@"c:\", @"c:\foo")); - Assert.False(EventProcessor.IsParent(@"c:\", @"c:\")); + Assert.False(EventNormalizer.IsParent(@"c:\", @"c:\foo")); + Assert.False(EventNormalizer.IsParent(@"c:\", @"c:\")); } [Fact] @@ -275,7 +275,7 @@ public void Parent_Dir_Is_Detected() FullPath = @"c:\foo" }; - Assert.True(EventProcessor.IsParent(ev, new List())); + Assert.True(EventNormalizer.IsParent(ev, new List())); } [Fact] @@ -289,7 +289,7 @@ public void Delete_Event_For_Subdirectory_Is_Detected() FullPath = @"c:\foo" }; - Assert.True(EventProcessor.IsParent(parentDirEvent, deletedFiles)); + Assert.True(EventNormalizer.IsParent(parentDirEvent, deletedFiles)); var subDirEvent = new FileChangedEvent @@ -298,11 +298,11 @@ public void Delete_Event_For_Subdirectory_Is_Detected() FullPath = @"c:\foo\bar" }; - Assert.False(EventProcessor.IsParent(subDirEvent, deletedFiles)); + Assert.False(EventNormalizer.IsParent(subDirEvent, deletedFiles)); } private static List NormalizeEvents(params FileChangedEvent[] events) { - return EventProcessor.NormalizeEvents(events).ToList(); + return new EventNormalizer().Normalize(events).ToList(); } } \ No newline at end of file From 75ce55ad41f5a281f9c2b88cad5e61e434267741 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Tue, 20 Dec 2022 17:01:41 +0100 Subject: [PATCH 44/92] fix inconsistency + refactor test --- .../FileWatcherEx/Helpers/EventNormalizer.cs | 7 +++- .../FileWatcherExTests/EventNormalizerTest.cs | 40 +++++++++---------- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/EventNormalizer.cs b/Source/FileWatcherEx/Helpers/EventNormalizer.cs index 88b3733..85f0578 100644 --- a/Source/FileWatcherEx/Helpers/EventNormalizer.cs +++ b/Source/FileWatcherEx/Helpers/EventNormalizer.cs @@ -125,9 +125,12 @@ internal static bool IsParent(FileChangedEvent e, List deletedPaths) } - internal static bool IsParent(string p, string candidate) + internal static bool IsParent(string path, string candidatePath) { - return p.IndexOf(candidate + '\\', StringComparison.Ordinal) == 0; + // if exists, remove trailing "\" for both paths + candidatePath = candidatePath.TrimEnd('\\'); + path = path.TrimEnd('\\'); + return path.IndexOf(candidatePath + '\\', StringComparison.Ordinal) == 0; } diff --git a/Source/FileWatcherExTests/EventNormalizerTest.cs b/Source/FileWatcherExTests/EventNormalizerTest.cs index 1cf5bb1..089e36d 100644 --- a/Source/FileWatcherExTests/EventNormalizerTest.cs +++ b/Source/FileWatcherExTests/EventNormalizerTest.cs @@ -143,7 +143,7 @@ public void Rename_After_Create_Gives_Changed_Event() Assert.Null(ev.OldFullPath); } -[Fact] + [Fact] public void Result_Suppressed_If_Delete_After_Create() { var events = NormalizeEvents( @@ -163,7 +163,7 @@ public void Result_Suppressed_If_Delete_After_Create() Assert.Empty(events); } - + [Fact] public void Changed_Event_After_Created_Is_Ignored() @@ -251,19 +251,19 @@ public void Filter_Out_Deleted_Event_With_Subdirectory() Assert.Equal(ChangeType.CREATED, filtered[1].ChangeType); Assert.Equal(@"c:\foo", filtered[1].FullPath); } - - [Fact] - public void Is_Parent() - { - Assert.True(EventNormalizer.IsParent(@"c:\a\b", @"c:")); - Assert.True(EventNormalizer.IsParent(@"c:\a\b", @"c:\a")); - // candidate must not have backslash - Assert.False(EventNormalizer.IsParent(@"c:\a\b", @"c:\")); - Assert.False(EventNormalizer.IsParent(@"c:\a\b", @"c:\a\")); - - Assert.False(EventNormalizer.IsParent(@"c:\", @"c:\foo")); - Assert.False(EventNormalizer.IsParent(@"c:\", @"c:\")); + [Theory] + [InlineData(@"c:\a\b", @"c:", true)] + [InlineData(@"c:\a\b", @"c:\a", true)] + [InlineData(@"c:\a\b\", @"c:", true)] + [InlineData(@"c:\a\b\", @"c:\a", true)] + [InlineData(@"c:\a\b", @"c:\", true)] + [InlineData(@"c:\a\b", @"c:\a\", true)] + [InlineData(@"c:\", @"c:\foo", false)] + [InlineData(@"c:\", @"c:\", false)] + public void Is_Parent(string path, string candidatePath, bool expectedResult) + { + Assert.Equal(expectedResult, EventNormalizer.IsParent(path, candidatePath)); } [Fact] @@ -277,30 +277,30 @@ public void Parent_Dir_Is_Detected() Assert.True(EventNormalizer.IsParent(ev, new List())); } - + [Fact] public void Delete_Event_For_Subdirectory_Is_Detected() { var deletedFiles = new List(); - + var parentDirEvent = new FileChangedEvent { ChangeType = ChangeType.DELETED, FullPath = @"c:\foo" }; - + Assert.True(EventNormalizer.IsParent(parentDirEvent, deletedFiles)); - + var subDirEvent = new FileChangedEvent { ChangeType = ChangeType.DELETED, FullPath = @"c:\foo\bar" }; - + Assert.False(EventNormalizer.IsParent(subDirEvent, deletedFiles)); } - + private static List NormalizeEvents(params FileChangedEvent[] events) { return new EventNormalizer().Normalize(events).ToList(); From 384874fb768b8853c03a36722aee5dc1c62f0c06 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Tue, 20 Dec 2022 17:13:39 +0100 Subject: [PATCH 45/92] time warp missing test case --- .../FileWatcherExTests/EventNormalizerTest.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Source/FileWatcherExTests/EventNormalizerTest.cs b/Source/FileWatcherExTests/EventNormalizerTest.cs index 089e36d..44e2e5a 100644 --- a/Source/FileWatcherExTests/EventNormalizerTest.cs +++ b/Source/FileWatcherExTests/EventNormalizerTest.cs @@ -164,7 +164,31 @@ public void Result_Suppressed_If_Delete_After_Create() Assert.Empty(events); } + [Fact] + public void Created_Event_After_Deleted_Results_Into_Changed() + { + var events = NormalizeEvents( + new FileChangedEvent + { + ChangeType = ChangeType.DELETED, + FullPath = @"c:\foo", + OldFullPath = null + }, + new FileChangedEvent + { + ChangeType = ChangeType.CREATED, + FullPath = @"c:\foo", + OldFullPath = null + } + ); + Assert.Single(events); + var ev = events.First(); + Assert.Equal(ChangeType.CHANGED, ev.ChangeType); + Assert.Equal(@"c:\foo", ev.FullPath); + Assert.Null(ev.OldFullPath); + } + [Fact] public void Changed_Event_After_Created_Is_Ignored() { From 6fd5dd30452fe08c83602426e5dff036c332c15f Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Tue, 20 Dec 2022 18:02:48 +0100 Subject: [PATCH 46/92] extract spam warning --- .../FileWatcherEx/Helpers/EventProcessor.cs | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index f99977a..36ab310 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -12,9 +12,9 @@ internal class EventProcessor private const int EventDelay = 50; /// - /// Warn after certain time span of event spam (in ticks) + /// Warn after certain time span of event spam /// - private const int EventSpamWarningThreshold = 60 * 1000 * 10000; + private readonly TimeSpan _eventSpamWarningThreshold = TimeSpan.FromMinutes(1); private readonly object _lock = new(); private Task? _delayTask = null; @@ -42,20 +42,7 @@ public void ProcessEvent(FileChangedEvent fileEvent) lock (_lock) { var now = DateTime.Now.Ticks; - - // Check for spam - if (_events.Count == 0) - { - _spamWarningLogged = false; - _spamCheckStartTime = now; - } - else if (!_spamWarningLogged && _spamCheckStartTime + EventSpamWarningThreshold < now) - { - _spamWarningLogged = true; - _logger(string.Format( - "Warning: Watcher is busy catching up with {0} file changes in 60 seconds. Latest path is '{1}'", - _events.Count, fileEvent.FullPath)); - } + CheckForSpam(fileEvent, now); // Add into our queue _events.Add(fileEvent); @@ -100,4 +87,19 @@ void Func(Task value) } } } + + private void CheckForSpam(FileChangedEvent fileEvent, long now) + { + if (_events.Count == 0) + { + _spamWarningLogged = false; + _spamCheckStartTime = now; + } + else if (! _spamWarningLogged && _spamCheckStartTime + _eventSpamWarningThreshold.Ticks < now) + { + _spamWarningLogged = true; + _logger($"Warning: Watcher is busy catching up with {_events.Count} file changes " + + $"in {_eventSpamWarningThreshold.TotalSeconds} seconds. Latest path is '{fileEvent.FullPath}'"); + } + } } \ No newline at end of file From f2c3e69e2209e0f80790c2b1fd11ce8fe8c629bb Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Tue, 20 Dec 2022 18:22:26 +0100 Subject: [PATCH 47/92] small refactoring --- .../FileWatcherEx/Helpers/EventProcessor.cs | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index 36ab310..cda59f2 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -42,7 +42,7 @@ public void ProcessEvent(FileChangedEvent fileEvent) lock (_lock) { var now = DateTime.Now.Ticks; - CheckForSpam(fileEvent, now); + WarnForSpam(fileEvent, now); // Add into our queue _events.Add(fileEvent); @@ -51,44 +51,43 @@ public void ProcessEvent(FileChangedEvent fileEvent) // Process queue after delay if (_delayTask == null) { - // Create function to buffer events - void Func(Task value) + // Start function after delay + _delayStarted = _lastEventTime; + _delayTask = Task.Delay(EventDelay).ContinueWith(HandleEventsFunc); + } + } + } + + private void HandleEventsFunc(Task _) + { + lock (_lock) + { + // Check if another event has been received in the meantime + if (_delayStarted == _lastEventTime) + { + // Normalize and handle + var normalized = new EventNormalizer().Normalize(_events.ToArray()); + foreach (var ev in normalized) { - lock (_lock) - { - // Check if another event has been received in the meantime - if (_delayStarted == _lastEventTime) - { - // Normalize and handle - var normalized = new EventNormalizer().Normalize(_events.ToArray()); - foreach (var e in normalized) - { - _handleEvent(e); - } - - // Reset - _events.Clear(); - _delayTask = null; - } - - // Otherwise we have received a new event while this task was - // delayed and we reschedule it. - else - { - _delayStarted = _lastEventTime; - _delayTask = Task.Delay(EventDelay).ContinueWith(Func); - } - } + _handleEvent(ev); } - // Start function after delay + // Reset + _events.Clear(); + _delayTask = null; + } + + // Otherwise we have received a new event while this task was + // delayed and we reschedule it. + else + { _delayStarted = _lastEventTime; - _delayTask = Task.Delay(EventDelay).ContinueWith(Func); + _delayTask = Task.Delay(EventDelay).ContinueWith(HandleEventsFunc); } } } - private void CheckForSpam(FileChangedEvent fileEvent, long now) + private void WarnForSpam(FileChangedEvent fileEvent, long now) { if (_events.Count == 0) { From c378218114e51d736e644b65c4dab7841d5765f6 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Tue, 20 Dec 2022 18:30:33 +0100 Subject: [PATCH 48/92] fix namespace --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 1 + Source/FileWatcherEx/Helpers/FileWatcher.cs | 2 +- Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 67c2be1..c15d489 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -1,6 +1,7 @@  using System.Collections.Concurrent; using System.ComponentModel; +using FileWatcherEx.Helpers; namespace FileWatcherEx; diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 663ca8f..8753480 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -2,7 +2,7 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -namespace FileWatcherEx; +namespace FileWatcherEx.Helpers; internal class FileWatcher : IDisposable { diff --git a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs index ab5e853..c1c5243 100644 --- a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs +++ b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs @@ -1,4 +1,5 @@ using FileWatcherEx; +using FileWatcherEx.Helpers; using FileWatcherExTests.Helper; using Moq; using Xunit; From b2d2e846f8602a5ff93656a97112f38db14ea26e Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Tue, 20 Dec 2022 18:37:49 +0100 Subject: [PATCH 49/92] IDE suggestions --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 58 ++++++++++----------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 8753480..d90ac60 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -55,14 +55,14 @@ public IFileSystemWatcherWrapper Create(string path, Action on | NotifyFilters.DirectoryName; // Bind internal events to manipulate the possible symbolic links - watcher.Created += new(MakeWatcher_Created); - watcher.Deleted += new(MakeWatcher_Deleted); + watcher.Created += MakeWatcher_Created; + watcher.Deleted += MakeWatcher_Deleted; - watcher.Changed += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CHANGED)); - watcher.Created += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CREATED)); - watcher.Deleted += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.DELETED)); - watcher.Renamed += new((object _, RenamedEventArgs e) => ProcessEvent(e)); - watcher.Error += new ErrorEventHandler((object source, ErrorEventArgs e) => onError(e)); + watcher.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); + watcher.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); + watcher.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); + watcher.Renamed += (_, e) => ProcessEvent(e); + watcher.Error += (source, e) => onError(e); //changing this to a higher value can lead into issues when watching UNC drives watcher.InternalBufferSize = 32768; @@ -136,14 +136,14 @@ private void MakeWatcher(string path) fileSystemWatcherRoot.EnableRaisingEvents = true; // Bind internal events to manipulate the possible symbolic links - fileSystemWatcherRoot.Created += new(MakeWatcher_Created); - fileSystemWatcherRoot.Deleted += new(MakeWatcher_Deleted); + fileSystemWatcherRoot.Created += MakeWatcher_Created; + fileSystemWatcherRoot.Deleted += MakeWatcher_Deleted; - fileSystemWatcherRoot.Changed += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CHANGED)); - fileSystemWatcherRoot.Created += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CREATED)); - fileSystemWatcherRoot.Deleted += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.DELETED)); - fileSystemWatcherRoot.Renamed += new((object _, RenamedEventArgs e) => ProcessEvent(e)); - fileSystemWatcherRoot.Error += new((object _, ErrorEventArgs e) => _onError?.Invoke(e)); + fileSystemWatcherRoot.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); + fileSystemWatcherRoot.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); + fileSystemWatcherRoot.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); + fileSystemWatcherRoot.Renamed += (_, e) => ProcessEvent(e); + fileSystemWatcherRoot.Error += (_, e) => _onError?.Invoke(e); _fwDictionary.Add(path, fileSystemWatcherRoot); } @@ -164,14 +164,14 @@ private void MakeWatcher(string path) fswItem.EnableRaisingEvents = true; // Bind internal events to manipulate the possible symbolic links - fswItem.Created += new(MakeWatcher_Created); - fswItem.Deleted += new(MakeWatcher_Deleted); + fswItem.Created += MakeWatcher_Created; + fswItem.Deleted += MakeWatcher_Deleted; - fswItem.Changed += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CHANGED)); - fswItem.Created += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CREATED)); - fswItem.Deleted += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.DELETED)); - fswItem.Renamed += new((object _, RenamedEventArgs e) => ProcessEvent(e)); - fswItem.Error += new((object _, ErrorEventArgs e) => _onError?.Invoke(e)); + fswItem.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); + fswItem.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); + fswItem.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); + fswItem.Renamed += (_, e) => ProcessEvent(e); + fswItem.Error += (_, e) => _onError?.Invoke(e); _fwDictionary.Add(item.FullName, fswItem); } @@ -196,14 +196,14 @@ internal void MakeWatcher_Created(object sender, FileSystemEventArgs e) watcherCreated.EnableRaisingEvents = true; // Bind internal events to manipulate the possible symbolic links - watcherCreated.Created += new(MakeWatcher_Created); - watcherCreated.Deleted += new(MakeWatcher_Deleted); - - watcherCreated.Changed += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CHANGED)); - watcherCreated.Created += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.CREATED)); - watcherCreated.Deleted += new((object _, FileSystemEventArgs e) => ProcessEvent(e, ChangeType.DELETED)); - watcherCreated.Renamed += new((object _, RenamedEventArgs e) => ProcessEvent(e)); - watcherCreated.Error += new((object _, ErrorEventArgs e) => _onError?.Invoke(e)); + watcherCreated.Created += MakeWatcher_Created; + watcherCreated.Deleted += MakeWatcher_Deleted; + + watcherCreated.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); + watcherCreated.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); + watcherCreated.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); + watcherCreated.Renamed += (_, e) => ProcessEvent(e); + watcherCreated.Error += (_, e) => _onError?.Invoke(e); _fwDictionary.Add(e.FullPath, watcherCreated); } From 711f54851e18c943375266b24e25b6cca7e78521 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Tue, 20 Dec 2022 22:54:43 +0100 Subject: [PATCH 50/92] RegisterFileWatcher method --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 102 +++++++------------- 1 file changed, 33 insertions(+), 69 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index d90ac60..b614745 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -47,27 +47,7 @@ public IFileSystemWatcherWrapper Create(string path, Action on _eventCallback = onEvent; _onError = onError; - var watcher = watcherFactory(); - watcher.Path = _watchPath; - watcher.IncludeSubdirectories = true; - watcher.NotifyFilter = NotifyFilters.LastWrite - | NotifyFilters.FileName - | NotifyFilters.DirectoryName; - - // Bind internal events to manipulate the possible symbolic links - watcher.Created += MakeWatcher_Created; - watcher.Deleted += MakeWatcher_Deleted; - - watcher.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); - watcher.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); - watcher.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); - watcher.Renamed += (_, e) => ProcessEvent(e); - watcher.Error += (source, e) => onError(e); - - //changing this to a higher value can lead into issues when watching UNC drives - watcher.InternalBufferSize = 32768; - // root watcher - _fwDictionary.Add(path, watcher); + var watcher = RegisterFileWatcher(_watchPath, false); // this handles sub directories. Probably needs cleanup foreach (var dirInfo in GetDirectoryInfosFunc(path)) @@ -130,22 +110,7 @@ private void MakeWatcher(string path) { if (!_fwDictionary.ContainsKey(path)) { - var fileSystemWatcherRoot = _watcherFactory(); - fileSystemWatcherRoot.Path = path; - fileSystemWatcherRoot.IncludeSubdirectories = true; - fileSystemWatcherRoot.EnableRaisingEvents = true; - - // Bind internal events to manipulate the possible symbolic links - fileSystemWatcherRoot.Created += MakeWatcher_Created; - fileSystemWatcherRoot.Deleted += MakeWatcher_Deleted; - - fileSystemWatcherRoot.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); - fileSystemWatcherRoot.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); - fileSystemWatcherRoot.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); - fileSystemWatcherRoot.Renamed += (_, e) => ProcessEvent(e); - fileSystemWatcherRoot.Error += (_, e) => _onError?.Invoke(e); - - _fwDictionary.Add(path, fileSystemWatcherRoot); + RegisterFileWatcher(path); } foreach (var item in GetDirectoryInfosFunc(path)) @@ -158,22 +123,7 @@ private void MakeWatcher(string path) { if (!_fwDictionary.ContainsKey(item.FullName)) { - var fswItem = _watcherFactory(); - fswItem.Path = item.FullName; - fswItem.IncludeSubdirectories = true; - fswItem.EnableRaisingEvents = true; - - // Bind internal events to manipulate the possible symbolic links - fswItem.Created += MakeWatcher_Created; - fswItem.Deleted += MakeWatcher_Deleted; - - fswItem.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); - fswItem.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); - fswItem.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); - fswItem.Renamed += (_, e) => ProcessEvent(e); - fswItem.Error += (_, e) => _onError?.Invoke(e); - - _fwDictionary.Add(item.FullName, fswItem); + RegisterFileWatcher(item.FullName); } MakeWatcher(item.FullName); @@ -190,22 +140,7 @@ internal void MakeWatcher_Created(object sender, FileSystemEventArgs e) if (attrs.HasFlag(FileAttributes.Directory) && attrs.HasFlag(FileAttributes.ReparsePoint)) { - var watcherCreated = _watcherFactory(); - watcherCreated.Path = e.FullPath; - watcherCreated.IncludeSubdirectories = true; - watcherCreated.EnableRaisingEvents = true; - - // Bind internal events to manipulate the possible symbolic links - watcherCreated.Created += MakeWatcher_Created; - watcherCreated.Deleted += MakeWatcher_Deleted; - - watcherCreated.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); - watcherCreated.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); - watcherCreated.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); - watcherCreated.Renamed += (_, e) => ProcessEvent(e); - watcherCreated.Error += (_, e) => _onError?.Invoke(e); - - _fwDictionary.Add(e.FullPath, watcherCreated); + RegisterFileWatcher(e.FullPath); } } catch (Exception ex) @@ -214,7 +149,36 @@ internal void MakeWatcher_Created(object sender, FileSystemEventArgs e) } } + private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRaisingEvents = true) + { + var fileWatcher = _watcherFactory(); + fileWatcher.Path = path; + // this is identical to the default value: + // https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=net-7.0#property-value + fileWatcher.NotifyFilter = NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName; + + fileWatcher.IncludeSubdirectories = true; + fileWatcher.EnableRaisingEvents = enableRaisingEvents; + // Bind internal events to manipulate the possible symbolic links + fileWatcher.Created += MakeWatcher_Created; + fileWatcher.Deleted += MakeWatcher_Deleted; + + fileWatcher.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); + fileWatcher.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); + fileWatcher.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); + fileWatcher.Renamed += (_, e) => ProcessEvent(e); + fileWatcher.Error += (_, e) => _onError?.Invoke(e); + + //changing this to a higher value can lead into issues when watching UNC drives + fileWatcher.InternalBufferSize = 32768; + + _fwDictionary.Add(path, fileWatcher); + return fileWatcher; + } + internal void MakeWatcher_Deleted(object sender, FileSystemEventArgs e) { // If object removed, then I will dispose and remove them from dictionary From 914f956eded2f5c59c2ebdd944708740d068cc1a Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Tue, 20 Dec 2022 23:02:38 +0100 Subject: [PATCH 51/92] IsSymbolicLinkDirectory --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 27 +++++++++------------ 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index b614745..f602d32 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -47,18 +47,14 @@ public IFileSystemWatcherWrapper Create(string path, Action on _eventCallback = onEvent; _onError = onError; - var watcher = RegisterFileWatcher(_watchPath, false); + var watcher = RegisterFileWatcher(_watchPath, enableRaisingEvents: false); - // this handles sub directories. Probably needs cleanup foreach (var dirInfo in GetDirectoryInfosFunc(path)) { - var attrs = GetFileAttributesFunc(dirInfo.FullName); - // TODO: consider skipping hidden/system folders? // See IG Issue #405 comment below - // https://github.com/d2phap/ImageGlass/issues/405 - if (attrs.HasFlag(FileAttributes.Directory) - && attrs.HasFlag(FileAttributes.ReparsePoint)) + + if (IsSymbolicLinkDirectory(dirInfo.FullName)) { try { @@ -115,11 +111,7 @@ private void MakeWatcher(string path) foreach (var item in GetDirectoryInfosFunc(path)) { - var attrs = GetFileAttributesFunc(item.FullName); - - // If is a directory and symbolic link - if (attrs.HasFlag(FileAttributes.Directory) - && attrs.HasFlag(FileAttributes.ReparsePoint)) + if (IsSymbolicLinkDirectory(item.FullName)) { if (!_fwDictionary.ContainsKey(item.FullName)) { @@ -136,9 +128,7 @@ internal void MakeWatcher_Created(object sender, FileSystemEventArgs e) { try { - var attrs = GetFileAttributesFunc(e.FullPath); - if (attrs.HasFlag(FileAttributes.Directory) - && attrs.HasFlag(FileAttributes.ReparsePoint)) + if (IsSymbolicLinkDirectory(e.FullPath)) { RegisterFileWatcher(e.FullPath); } @@ -178,6 +168,13 @@ private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRa _fwDictionary.Add(path, fileWatcher); return fileWatcher; } + + private bool IsSymbolicLinkDirectory(string path) + { + var attrs = GetFileAttributesFunc(path); + return attrs.HasFlag(FileAttributes.Directory) + && attrs.HasFlag(FileAttributes.ReparsePoint); + } internal void MakeWatcher_Deleted(object sender, FileSystemEventArgs e) { From ceef99e794c2b712a77b74d542d09fa7c2da8e29 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Tue, 20 Dec 2022 23:37:52 +0100 Subject: [PATCH 52/92] refactor --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 148 +++++++++--------- .../FileSystemWatcherCreationTest.cs | 8 +- 2 files changed, 82 insertions(+), 74 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index f602d32..a4b5640 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -6,6 +6,7 @@ namespace FileWatcherEx.Helpers; internal class FileWatcher : IDisposable { + // TODO double check properties -> are they all needed ? private string _watchPath = string.Empty; private Action? _eventCallback = null; private readonly Dictionary _fwDictionary = new(); @@ -48,12 +49,50 @@ public IFileSystemWatcherWrapper Create(string path, Action on _onError = onError; var watcher = RegisterFileWatcher(_watchPath, enableRaisingEvents: false); + RegisterFileWatchersForSymbolicLinkDirs(path); + return watcher; + } + + private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRaisingEvents = true) + { + var fileWatcher = _watcherFactory(); + fileWatcher.Path = path; + // this is identical to the default value: + // https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=net-7.0#property-value + fileWatcher.NotifyFilter = NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName; + + fileWatcher.IncludeSubdirectories = true; + fileWatcher.EnableRaisingEvents = enableRaisingEvents; + + fileWatcher.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); + fileWatcher.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); + fileWatcher.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); + fileWatcher.Renamed += (_, e) => ProcessRenamedEvent(e); + fileWatcher.Error += (_, e) => _onError?.Invoke(e); + + // extra measures to handle symbolic link directories + fileWatcher.Created += RegisterWatcherForSymbolicLinkDir; + fileWatcher.Deleted += RemoveWatcherForSymbolicLinkDir; + + //changing this to a higher value can lead into issues when watching UNC drives + fileWatcher.InternalBufferSize = 32768; + + _fwDictionary.Add(path, fileWatcher); + return fileWatcher; + } + + + private void RegisterFileWatchersForSymbolicLinkDirs(string path) + { + // TODO check if test for nested sym links exists + // then make it just one recursive function foreach (var dirInfo in GetDirectoryInfosFunc(path)) { // TODO: consider skipping hidden/system folders? // See IG Issue #405 comment below - if (IsSymbolicLinkDirectory(dirInfo.FullName)) { try @@ -67,16 +106,26 @@ public IFileSystemWatcherWrapper Create(string path, Action on } } } - - return watcher; } + private void MakeWatcher(string path) + { + RegisterFileWatcherIfNotExists(path); + foreach (var item in GetDirectoryInfosFunc(path)) + { + if (IsSymbolicLinkDirectory(item.FullName)) + { + RegisterFileWatcherIfNotExists(item.FullName); + MakeWatcher(item.FullName); + } + } + } + + /// /// Process event for type = [CHANGED; DELETED; CREATED] /// - /// - /// private void ProcessEvent(FileSystemEventArgs e, ChangeType changeType) { _eventCallback?.Invoke(new() @@ -85,13 +134,9 @@ private void ProcessEvent(FileSystemEventArgs e, ChangeType changeType) FullPath = e.FullPath, }); } - - - /// - /// Process event for type = RENAMED - /// - /// - private void ProcessEvent(RenamedEventArgs e) + + + private void ProcessRenamedEvent(RenamedEventArgs e) { _eventCallback?.Invoke(new() { @@ -100,31 +145,29 @@ private void ProcessEvent(RenamedEventArgs e) OldFullPath = e.OldFullPath, }); } - - - private void MakeWatcher(string path) + + + private void RegisterFileWatcherIfNotExists(string path) { - if (!_fwDictionary.ContainsKey(path)) + if (! _fwDictionary.ContainsKey(path)) { RegisterFileWatcher(path); } - - foreach (var item in GetDirectoryInfosFunc(path)) - { - if (IsSymbolicLinkDirectory(item.FullName)) - { - if (!_fwDictionary.ContainsKey(item.FullName)) - { - RegisterFileWatcher(item.FullName); - } - - MakeWatcher(item.FullName); - } - } } - - internal void MakeWatcher_Created(object sender, FileSystemEventArgs e) + + private bool IsSymbolicLinkDirectory(string path) + { + var attrs = GetFileAttributesFunc(path); + return attrs.HasFlag(FileAttributes.Directory) + && attrs.HasFlag(FileAttributes.ReparsePoint); + } + + + /// + /// Register additional filewatcher if the file event is a symbolic link directory. + /// + internal void RegisterWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) { try { @@ -139,46 +182,11 @@ internal void MakeWatcher_Created(object sender, FileSystemEventArgs e) } } - private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRaisingEvents = true) - { - var fileWatcher = _watcherFactory(); - fileWatcher.Path = path; - // this is identical to the default value: - // https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=net-7.0#property-value - fileWatcher.NotifyFilter = NotifyFilters.LastWrite - | NotifyFilters.FileName - | NotifyFilters.DirectoryName; - - fileWatcher.IncludeSubdirectories = true; - fileWatcher.EnableRaisingEvents = enableRaisingEvents; - - // Bind internal events to manipulate the possible symbolic links - fileWatcher.Created += MakeWatcher_Created; - fileWatcher.Deleted += MakeWatcher_Deleted; - - fileWatcher.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); - fileWatcher.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); - fileWatcher.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); - fileWatcher.Renamed += (_, e) => ProcessEvent(e); - fileWatcher.Error += (_, e) => _onError?.Invoke(e); - - //changing this to a higher value can lead into issues when watching UNC drives - fileWatcher.InternalBufferSize = 32768; - - _fwDictionary.Add(path, fileWatcher); - return fileWatcher; - } - - private bool IsSymbolicLinkDirectory(string path) - { - var attrs = GetFileAttributesFunc(path); - return attrs.HasFlag(FileAttributes.Directory) - && attrs.HasFlag(FileAttributes.ReparsePoint); - } - - internal void MakeWatcher_Deleted(object sender, FileSystemEventArgs e) + /// + /// Cleanup filewatcher if a symbolic link dir is deleted + /// + internal void RemoveWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) { - // If object removed, then I will dispose and remove them from dictionary if (_fwDictionary.ContainsKey(e.FullPath)) { _fwDictionary[e.FullPath].Dispose(); diff --git a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs index c1c5243..a47d714 100644 --- a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs +++ b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs @@ -90,7 +90,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() Directory.CreateDirectory(subdirPath); // simulate file watcher trigger - _uut.MakeWatcher_Created(null, + _uut.RegisterWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Created, dir.FullPath, "subdir")); // subdir is ignored @@ -102,7 +102,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() Directory.CreateSymbolicLink(symlinkPath, subdirPath); // simulate file watcher trigger - _uut.MakeWatcher_Created(null, + _uut.RegisterWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Created, dir.FullPath, "sym")); // symlink dir is registered @@ -114,7 +114,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() Directory.Delete(symlinkPath); // simulate file watcher trigger - _uut.MakeWatcher_Deleted(null, + _uut.RemoveWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Deleted, dir.FullPath, "sym")); // sym-link file watcher is removed @@ -127,7 +127,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() [Fact] public void MakeWatcher_Create_Exceptions_Are_Silently_Ignored() { - _uut.MakeWatcher_Created(null, + _uut.RegisterWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Created, "/not/existing", "foo")); } From 83b8095674bd34bae78078c8dceaebbf5a2a39ce Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Wed, 21 Dec 2022 09:29:09 +0100 Subject: [PATCH 53/92] TODO --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index c15d489..ee9d05d 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -263,6 +263,11 @@ void onError(ErrorEventArgs e) } + // TODO + // - FileWatcher should not return underlying filewatcher + // - introduce stronger encapsulation + // - `watcher` should delegate the set properties to ALL file watchers + // - if you register a dir later, these 4 properties should be used // Start watcher _watcher = new FileWatcher(); From aa2fe5aa52deadcd6b5df7b6d5b3050b95d3990e Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Wed, 21 Dec 2022 10:01:34 +0100 Subject: [PATCH 54/92] rewrite directory discovery --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 111 ++++++++---------- .../FileSystemWatcherCreationTest.cs | 8 +- 2 files changed, 56 insertions(+), 63 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index a4b5640..01154c3 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -49,7 +49,7 @@ public IFileSystemWatcherWrapper Create(string path, Action on _onError = onError; var watcher = RegisterFileWatcher(_watchPath, enableRaisingEvents: false); - RegisterFileWatchersForSymbolicLinkDirs(path); + RegisterAdditionalFileWatchersForSymLinkDirs(path); return watcher; } @@ -74,8 +74,8 @@ private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRa fileWatcher.Error += (_, e) => _onError?.Invoke(e); // extra measures to handle symbolic link directories - fileWatcher.Created += RegisterWatcherForSymbolicLinkDir; - fileWatcher.Deleted += RemoveWatcherForSymbolicLinkDir; + fileWatcher.Created += AddFileWatcherForSymbolicLinkDir; + fileWatcher.Deleted += RemoveFileWatcherForSymbolicLinkDir; //changing this to a higher value can lead into issues when watching UNC drives fileWatcher.InternalBufferSize = 32768; @@ -85,43 +85,25 @@ private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRa } - private void RegisterFileWatchersForSymbolicLinkDirs(string path) + /// + /// Recursively find sym link dir and register them + /// + private void RegisterAdditionalFileWatchersForSymLinkDirs(string path) { - // TODO check if test for nested sym links exists - // then make it just one recursive function - foreach (var dirInfo in GetDirectoryInfosFunc(path)) + if (IsSymbolicLinkDirectory(path)) { - // TODO: consider skipping hidden/system folders? - // See IG Issue #405 comment below - if (IsSymbolicLinkDirectory(dirInfo.FullName)) - { - try - { - MakeWatcher(dirInfo.FullName); - } - catch - { - // IG Issue #405: throws exception on Windows 10 - // for "c:\users\user\application data" folder and sub-folders. - } - } + TryRegisterFileWatcher(path); } - } - - private void MakeWatcher(string path) - { - RegisterFileWatcherIfNotExists(path); - foreach (var item in GetDirectoryInfosFunc(path)) + if (IsDirectory(path)) { - if (IsSymbolicLinkDirectory(item.FullName)) + foreach (var dirInfo in GetDirectoryInfosFunc(path)) { - RegisterFileWatcherIfNotExists(item.FullName); - MakeWatcher(item.FullName); + RegisterAdditionalFileWatchersForSymLinkDirs(dirInfo.FullName); } } } - + /// /// Process event for type = [CHANGED; DELETED; CREATED] @@ -147,45 +129,21 @@ private void ProcessRenamedEvent(RenamedEventArgs e) } - private void RegisterFileWatcherIfNotExists(string path) - { - if (! _fwDictionary.ContainsKey(path)) - { - RegisterFileWatcher(path); - } - } - - - private bool IsSymbolicLinkDirectory(string path) - { - var attrs = GetFileAttributesFunc(path); - return attrs.HasFlag(FileAttributes.Directory) - && attrs.HasFlag(FileAttributes.ReparsePoint); - } - - /// /// Register additional filewatcher if the file event is a symbolic link directory. /// - internal void RegisterWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) + internal void AddFileWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) { - try - { - if (IsSymbolicLinkDirectory(e.FullPath)) - { - RegisterFileWatcher(e.FullPath); - } - } - catch (Exception ex) + if (IsSymbolicLinkDirectory(e.FullPath)) { - Console.WriteLine("<>: " + ex.Message); + TryRegisterFileWatcher(e.FullPath); } } /// /// Cleanup filewatcher if a symbolic link dir is deleted /// - internal void RemoveWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) + internal void RemoveFileWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) { if (_fwDictionary.ContainsKey(e.FullPath)) { @@ -193,6 +151,41 @@ internal void RemoveWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs _fwDictionary.Remove(e.FullPath); } } + + + private void TryRegisterFileWatcher(string path) + { + if (! _fwDictionary.ContainsKey(path)) + { + try + { + RegisterFileWatcher(path); + } + catch (Exception ex) + { + // IG Issue #405: throws exception on Windows 10 + // for "c:\users\user\application data" folder and sub-folders. + + // TODO pass in log action + // Console.Error.WriteLine($"Error registering file system watcher for directory '{path}'. Error was: {ex.Message}"); + } + } + } + + + private bool IsSymbolicLinkDirectory(string path) + { + var attrs = GetFileAttributesFunc(path); + return attrs.HasFlag(FileAttributes.Directory) + && attrs.HasFlag(FileAttributes.ReparsePoint); + } + + + private bool IsDirectory(string path) + { + var attrs = GetFileAttributesFunc(path); + return attrs.HasFlag(FileAttributes.Directory); + } /// diff --git a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs index a47d714..ec214c1 100644 --- a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs +++ b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs @@ -90,7 +90,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() Directory.CreateDirectory(subdirPath); // simulate file watcher trigger - _uut.RegisterWatcherForSymbolicLinkDir(null, + _uut.AddFileWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Created, dir.FullPath, "subdir")); // subdir is ignored @@ -102,7 +102,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() Directory.CreateSymbolicLink(symlinkPath, subdirPath); // simulate file watcher trigger - _uut.RegisterWatcherForSymbolicLinkDir(null, + _uut.AddFileWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Created, dir.FullPath, "sym")); // symlink dir is registered @@ -114,7 +114,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() Directory.Delete(symlinkPath); // simulate file watcher trigger - _uut.RemoveWatcherForSymbolicLinkDir(null, + _uut.RemoveFileWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Deleted, dir.FullPath, "sym")); // sym-link file watcher is removed @@ -127,7 +127,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() [Fact] public void MakeWatcher_Create_Exceptions_Are_Silently_Ignored() { - _uut.RegisterWatcherForSymbolicLinkDir(null, + _uut.AddFileWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Created, "/not/existing", "foo")); } From e69cb80d976f978e1fae87ae0515c2e49267bd03 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Wed, 21 Dec 2022 10:28:33 +0100 Subject: [PATCH 55/92] refactor --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 2 +- Source/FileWatcherEx/Helpers/FileWatcher.cs | 65 +++++++------------ .../FileSystemWatcherCreationTest.cs | 32 ++++----- 3 files changed, 39 insertions(+), 60 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index ee9d05d..817c332 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -271,7 +271,7 @@ void onError(ErrorEventArgs e) // Start watcher _watcher = new FileWatcher(); - _fsw = _watcher.Create(FolderPath, onEvent, onError, FileSystemWatcherFactory); + _fsw = _watcher.Create(FolderPath, onEvent, onError, FileSystemWatcherFactory, _ => {}); foreach (var filter in Filters) { diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 01154c3..b8b15cf 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -14,6 +14,7 @@ internal class FileWatcher : IDisposable private Func? _getFileAttributesFunc; private Func? _getDirectoryInfosFunc; private Func _watcherFactory; + private Action _logger = _ => {}; internal Func GetFileAttributesFunc { @@ -40,9 +41,11 @@ internal Func GetDirectoryInfosFunc /// onEvent callback /// onError callback /// how to create a FileSystemWatcher + /// logging callback /// - public IFileSystemWatcherWrapper Create(string path, Action onEvent, Action onError, Func watcherFactory) + public IFileSystemWatcherWrapper Create(string path, Action onEvent, Action onError, Func watcherFactory, Action logger) { + _logger = logger; _watcherFactory = watcherFactory; _watchPath = path; _eventCallback = onEvent; @@ -74,8 +77,8 @@ private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRa fileWatcher.Error += (_, e) => _onError?.Invoke(e); // extra measures to handle symbolic link directories - fileWatcher.Created += AddFileWatcherForSymbolicLinkDir; - fileWatcher.Deleted += RemoveFileWatcherForSymbolicLinkDir; + fileWatcher.Created += (_, e) => TryRegisterFileWatcherForSymbolicLinkDir(e.FullPath); + fileWatcher.Deleted += UnregisterFileWatcherForSymbolicLinkDir; //changing this to a higher value can lead into issues when watching UNC drives fileWatcher.InternalBufferSize = 32768; @@ -90,12 +93,9 @@ private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRa /// private void RegisterAdditionalFileWatchersForSymLinkDirs(string path) { - if (IsSymbolicLinkDirectory(path)) - { - TryRegisterFileWatcher(path); - } + TryRegisterFileWatcherForSymbolicLinkDir(path); - if (IsDirectory(path)) + if (Directory.Exists(path)) { foreach (var dirInfo in GetDirectoryInfosFunc(path)) { @@ -129,21 +129,28 @@ private void ProcessRenamedEvent(RenamedEventArgs e) } - /// - /// Register additional filewatcher if the file event is a symbolic link directory. - /// - internal void AddFileWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) + internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) { - if (IsSymbolicLinkDirectory(e.FullPath)) + try + { + if ( IsSymbolicLinkDirectory(path) && !_fwDictionary.ContainsKey(path) ) + { + RegisterFileWatcher(path); + } + } + catch (Exception ex) { - TryRegisterFileWatcher(e.FullPath); + // IG Issue #405: throws exception on Windows 10 + // for "c:\users\user\application data" folder and sub-folders. + _logger($"Error registering file system watcher for directory '{path}'. Error was: {ex.Message}"); } } + /// /// Cleanup filewatcher if a symbolic link dir is deleted /// - internal void RemoveFileWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) + internal void UnregisterFileWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) { if (_fwDictionary.ContainsKey(e.FullPath)) { @@ -153,26 +160,6 @@ internal void RemoveFileWatcherForSymbolicLinkDir(object sender, FileSystemEvent } - private void TryRegisterFileWatcher(string path) - { - if (! _fwDictionary.ContainsKey(path)) - { - try - { - RegisterFileWatcher(path); - } - catch (Exception ex) - { - // IG Issue #405: throws exception on Windows 10 - // for "c:\users\user\application data" folder and sub-folders. - - // TODO pass in log action - // Console.Error.WriteLine($"Error registering file system watcher for directory '{path}'. Error was: {ex.Message}"); - } - } - } - - private bool IsSymbolicLinkDirectory(string path) { var attrs = GetFileAttributesFunc(path); @@ -180,14 +167,6 @@ private bool IsSymbolicLinkDirectory(string path) && attrs.HasFlag(FileAttributes.ReparsePoint); } - - private bool IsDirectory(string path) - { - var attrs = GetFileAttributesFunc(path); - return attrs.HasFlag(FileAttributes.Directory); - } - - /// /// Dispose the instance /// diff --git a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs index ec214c1..69f3c81 100644 --- a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs +++ b/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs @@ -28,9 +28,10 @@ public void Root_Watcher_Is_Created() _uut.Create( dir.FullPath, - e => {}, - e => {}, - WatcherFactoryWithMemory); + _ => {}, + _ => {}, + WatcherFactoryWithMemory, + _ => {}); AssertContainsWatcherFor(dir.FullPath); } @@ -66,9 +67,10 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() _uut.Create( dir.FullPath, - e => {}, - e => {}, - WatcherFactoryWithMemory); + _ => {}, + _ => {}, + WatcherFactoryWithMemory, + _ => {}); AssertContainsWatcherFor(dir.FullPath); AssertContainsWatcherFor(symlinkPath1); @@ -81,17 +83,17 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() using var dir = new TempDir(); _uut.Create( dir.FullPath, - e => {}, - e => {}, - WatcherFactoryWithMemory); + _ => {}, + _ => {}, + WatcherFactoryWithMemory, + _ => {}); // create subdir var subdirPath = Path.Combine(dir.FullPath, "subdir"); Directory.CreateDirectory(subdirPath); // simulate file watcher trigger - _uut.AddFileWatcherForSymbolicLinkDir(null, - new FileSystemEventArgs(WatcherChangeTypes.Created, dir.FullPath, "subdir")); + _uut.TryRegisterFileWatcherForSymbolicLinkDir(subdirPath); // subdir is ignored Assert.Single(_uut.FwDictionary); @@ -102,8 +104,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() Directory.CreateSymbolicLink(symlinkPath, subdirPath); // simulate file watcher trigger - _uut.AddFileWatcherForSymbolicLinkDir(null, - new FileSystemEventArgs(WatcherChangeTypes.Created, dir.FullPath, "sym")); + _uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath); // symlink dir is registered Assert.Equal(2, _uut.FwDictionary.Count); @@ -114,7 +115,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() Directory.Delete(symlinkPath); // simulate file watcher trigger - _uut.RemoveFileWatcherForSymbolicLinkDir(null, + _uut.UnregisterFileWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Deleted, dir.FullPath, "sym")); // sym-link file watcher is removed @@ -127,8 +128,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() [Fact] public void MakeWatcher_Create_Exceptions_Are_Silently_Ignored() { - _uut.AddFileWatcherForSymbolicLinkDir(null, - new FileSystemEventArgs(WatcherChangeTypes.Created, "/not/existing", "foo")); + _uut.TryRegisterFileWatcherForSymbolicLinkDir("/not/existing/foo"); } private void AssertContainsWatcherFor(string path) From ffd18c4bab41e6cd8c174f2934662047bfb27d5e Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 22 Dec 2022 10:57:25 +0100 Subject: [PATCH 56/92] split "create" into constructor and "init" --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 11 +++- Source/FileWatcherEx/Helpers/FileWatcher.cs | 13 ++-- ...cherCreationTest.cs => FileWatcherTest.cs} | 59 ++++++++----------- 3 files changed, 42 insertions(+), 41 deletions(-) rename Source/FileWatcherExTests/{FileSystemWatcherCreationTest.cs => FileWatcherTest.cs} (88%) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 817c332..f7ed9a4 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -269,19 +269,24 @@ void onError(ErrorEventArgs e) // - `watcher` should delegate the set properties to ALL file watchers // - if you register a dir later, these 4 properties should be used // Start watcher - _watcher = new FileWatcher(); - - _fsw = _watcher.Create(FolderPath, onEvent, onError, FileSystemWatcherFactory, _ => {}); + _watcher = new FileWatcher(FolderPath, onEvent, onError, FileSystemWatcherFactory, _ => {}); + _fsw = _watcher.Init(); + // all foreach (var filter in Filters) { _fsw.Filters.Add(filter); } + // all _fsw.NotifyFilter = NotifyFilter; + // all. if this is not enabled, then also no additional file watchers should be registered _fsw.IncludeSubdirectories = IncludeSubdirectories; + + // exception: only root watcher _fsw.SynchronizingObject = SynchronizingObject; + // global // Start watching _fsw.EnableRaisingEvents = true; } diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index b8b15cf..99a7cbf 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -42,17 +42,19 @@ internal Func GetDirectoryInfosFunc /// onError callback /// how to create a FileSystemWatcher /// logging callback - /// - public IFileSystemWatcherWrapper Create(string path, Action onEvent, Action onError, Func watcherFactory, Action logger) + public FileWatcher(string path, Action onEvent, Action onError, Func watcherFactory, Action logger) { _logger = logger; _watcherFactory = watcherFactory; _watchPath = path; _eventCallback = onEvent; _onError = onError; - + } + + public IFileSystemWatcherWrapper Init() + { var watcher = RegisterFileWatcher(_watchPath, enableRaisingEvents: false); - RegisterAdditionalFileWatchersForSymLinkDirs(path); + RegisterAdditionalFileWatchersForSymLinkDirs(_watchPath); return watcher; } @@ -89,7 +91,8 @@ private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRa /// - /// Recursively find sym link dir and register them + /// Recursively find sym link dir and register them. + /// Background: the native filewatcher does not follow symlinks so they need to be treated separately. /// private void RegisterAdditionalFileWatchersForSymLinkDirs(string path) { diff --git a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs b/Source/FileWatcherExTests/FileWatcherTest.cs similarity index 88% rename from Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs rename to Source/FileWatcherExTests/FileWatcherTest.cs index 69f3c81..c88ec45 100644 --- a/Source/FileWatcherExTests/FileSystemWatcherCreationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherTest.cs @@ -7,32 +7,23 @@ namespace FileWatcherExTests; -public class FileSystemWatcherCreationTest +public class FileWatcherTest { private readonly ITestOutputHelper _testOutputHelper; - private readonly FileWatcher _uut; + private FileWatcher _uut; private readonly List> _mocks; - public FileSystemWatcherCreationTest(ITestOutputHelper testOutputHelper) + public FileWatcherTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; - _uut = new FileWatcher(); _mocks = new List>(); - } [Fact] public void Root_Watcher_Is_Created() { using var dir = new TempDir(); - - _uut.Create( - dir.FullPath, - _ => {}, - _ => {}, - WatcherFactoryWithMemory, - _ => {}); - + _uut = CreateFileWatcher(dir.FullPath); AssertContainsWatcherFor(dir.FullPath); } @@ -65,13 +56,8 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() var symlinkPath2 = Path.Combine(dir.FullPath, "sym1", "sym2"); Directory.CreateSymbolicLink(symlinkPath2, subdirPath2); - _uut.Create( - dir.FullPath, - _ => {}, - _ => {}, - WatcherFactoryWithMemory, - _ => {}); - + _uut = CreateFileWatcher(dir.FullPath); + AssertContainsWatcherFor(dir.FullPath); AssertContainsWatcherFor(symlinkPath1); AssertContainsWatcherFor(symlinkPath2); @@ -81,12 +67,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() { using var dir = new TempDir(); - _uut.Create( - dir.FullPath, - _ => {}, - _ => {}, - WatcherFactoryWithMemory, - _ => {}); + _uut = CreateFileWatcher(dir.FullPath); // create subdir var subdirPath = Path.Combine(dir.FullPath, "subdir"); @@ -128,9 +109,28 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() [Fact] public void MakeWatcher_Create_Exceptions_Are_Silently_Ignored() { + _uut = CreateFileWatcher("/bar"); _uut.TryRegisterFileWatcherForSymbolicLinkDir("/not/existing/foo"); } + private FileWatcher CreateFileWatcher(string path) + { + var fw = new FileWatcher(path, + _ => {}, + _ => {}, + WatcherFactoryWithMemory, + _ => {}); + fw.Init(); + return fw; + } + + private IFileSystemWatcherWrapper WatcherFactoryWithMemory() + { + var mock = new Mock(); + _mocks.Add(mock); + return mock.Object; + } + private void AssertContainsWatcherFor(string path) { var _ = _uut.FwDictionary[path]; @@ -154,11 +154,4 @@ private bool IsMockFor(Mock mock, string path) return false; } } - - private IFileSystemWatcherWrapper WatcherFactoryWithMemory() - { - var mock = new Mock(); - _mocks.Add(mock); - return mock.Object; - } } \ No newline at end of file From 6ae08a5d9fef179a15373f68febcf7599b149052 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Fri, 23 Dec 2022 00:00:39 +0100 Subject: [PATCH 57/92] towards properties are propagated --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 32 +++++++-- Source/FileWatcherExTests/FileWatcherTest.cs | 76 ++++++++++++++------ Source/FileWatcherExTests/Helper/TempDir.cs | 15 ++++ 3 files changed, 95 insertions(+), 28 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 99a7cbf..ecc8cce 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -16,6 +16,12 @@ internal class FileWatcher : IDisposable private Func _watcherFactory; private Action _logger = _ => {}; + // defaults from: + // https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=net-7.0#property-value + private NotifyFilters _notifyFilters = NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName; + internal Func GetFileAttributesFunc { get => _getFileAttributesFunc ?? File.GetAttributes; @@ -33,6 +39,18 @@ internal Func GetDirectoryInfosFunc } internal Dictionary FwDictionary => _fwDictionary; + public NotifyFilters NotifyFilter + { + set + { + _notifyFilters = value; + + foreach (var watcher in _fwDictionary.Values) + { + watcher.NotifyFilter = value; + } + } + } /// /// Create new instance of FileSystemWatcherWrapper @@ -63,12 +81,7 @@ private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRa { var fileWatcher = _watcherFactory(); fileWatcher.Path = path; - // this is identical to the default value: - // https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=net-7.0#property-value - fileWatcher.NotifyFilter = NotifyFilters.LastWrite - | NotifyFilters.FileName - | NotifyFilters.DirectoryName; - + fileWatcher.NotifyFilter = _notifyFilters; fileWatcher.IncludeSubdirectories = true; fileWatcher.EnableRaisingEvents = enableRaisingEvents; @@ -169,6 +182,13 @@ private bool IsSymbolicLinkDirectory(string path) return attrs.HasFlag(FileAttributes.Directory) && attrs.HasFlag(FileAttributes.ReparsePoint); } + + // for testing + internal List GetFileWatchers() + { + return _fwDictionary.Values.ToList(); + } + /// /// Dispose the instance diff --git a/Source/FileWatcherExTests/FileWatcherTest.cs b/Source/FileWatcherExTests/FileWatcherTest.cs index c88ec45..875ee11 100644 --- a/Source/FileWatcherExTests/FileWatcherTest.cs +++ b/Source/FileWatcherExTests/FileWatcherTest.cs @@ -27,7 +27,7 @@ public void Root_Watcher_Is_Created() AssertContainsWatcherFor(dir.FullPath); } - + // creates 2 sub-directories and 2 nested symlinks // file watchers are registered for the root dir and the symlinks // for the sub-directories, no extra file watchers are created @@ -39,7 +39,7 @@ public void Root_Watcher_Is_Created() public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() { using var dir = new TempDir(); - + // {tempdir}/subdir1 var subdirPath1 = Path.Combine(dir.FullPath, "subdir1"); Directory.CreateDirectory(subdirPath1); @@ -51,7 +51,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() // symlink {tempdir}/sym1 to {tempdir}/subdir1 var symlinkPath1 = Path.Combine(dir.FullPath, "sym1"); Directory.CreateSymbolicLink(symlinkPath1, subdirPath1); - + // symlink {tempdir}/sym1/sym2 to {tempdir}/subdir2 var symlinkPath2 = Path.Combine(dir.FullPath, "sym1", "sym2"); Directory.CreateSymbolicLink(symlinkPath2, subdirPath2); @@ -62,17 +62,17 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() AssertContainsWatcherFor(symlinkPath1); AssertContainsWatcherFor(symlinkPath2); } - + [Fact] public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() { using var dir = new TempDir(); _uut = CreateFileWatcher(dir.FullPath); - + // create subdir var subdirPath = Path.Combine(dir.FullPath, "subdir"); Directory.CreateDirectory(subdirPath); - + // simulate file watcher trigger _uut.TryRegisterFileWatcherForSymbolicLinkDir(subdirPath); @@ -94,15 +94,15 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() // remove the symlink again Directory.Delete(symlinkPath); - + // simulate file watcher trigger - _uut.UnregisterFileWatcherForSymbolicLinkDir(null, + _uut.UnregisterFileWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Deleted, dir.FullPath, "sym")); - + // sym-link file watcher is removed Assert.Single(_uut.FwDictionary); AssertContainsWatcherFor(dir.FullPath); - + _uut.Dispose(); } @@ -112,18 +112,49 @@ public void MakeWatcher_Create_Exceptions_Are_Silently_Ignored() _uut = CreateFileWatcher("/bar"); _uut.TryRegisterFileWatcherForSymbolicLinkDir("/not/existing/foo"); } - + + [Fact] + public void Properties_Are_Propagated() + { + using var dir = new TempDir(); + + var subDir = dir.CreateSubDir("subdir"); + + // symlink at startup detected + dir.CreateSymlink( + symLink: "sym1", + target: subDir); + + // start the watcher + configure + _uut = CreateFileWatcher(dir.FullPath); + _uut.NotifyFilter = NotifyFilters.LastAccess; + + // create symlink during runtime + var symlinkPath2 = dir.CreateSymlink( + symLink: "sym2", + target: subDir); + + // simulate file watcher trigger + _uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath2); + + Assert.Equal(3, _mocks.Count); + Assert.All( + _mocks, + mock => + mock.VerifySet(w => w.NotifyFilter = NotifyFilters.LastAccess)); + } + private FileWatcher CreateFileWatcher(string path) { - var fw = new FileWatcher(path, - _ => {}, - _ => {}, + var fw = new FileWatcher(path, + _ => { }, + _ => { }, WatcherFactoryWithMemory, - _ => {}); + _ => { }); fw.Init(); return fw; } - + private IFileSystemWatcherWrapper WatcherFactoryWithMemory() { var mock = new Mock(); @@ -135,18 +166,18 @@ private void AssertContainsWatcherFor(string path) { var _ = _uut.FwDictionary[path]; var foundMocks = ( - from mock in _mocks - where IsMockFor(mock, path) - select mock) + from mock in _mocks + where HasPropertySetTo(mock, watcher => watcher.Path = path) + select mock) .Count(); - Assert.Equal(1, foundMocks); + Assert.Equal(1, foundMocks); } - private bool IsMockFor(Mock mock, string path) + private static bool HasPropertySetTo(Mock mock, Action setterExpression) { try { - mock.VerifySet(w => w.Path = path); + mock.VerifySet(setterExpression); return true; } catch (MockException) @@ -154,4 +185,5 @@ private bool IsMockFor(Mock mock, string path) return false; } } + } \ No newline at end of file diff --git a/Source/FileWatcherExTests/Helper/TempDir.cs b/Source/FileWatcherExTests/Helper/TempDir.cs index 4c445f1..ee01bea 100644 --- a/Source/FileWatcherExTests/Helper/TempDir.cs +++ b/Source/FileWatcherExTests/Helper/TempDir.cs @@ -10,6 +10,21 @@ public TempDir() Directory.CreateDirectory(FullPath); } + public string CreateSubDir(string path) + { + var subDirPath = Path.Combine(FullPath, path); + Directory.CreateDirectory(subDirPath); + return subDirPath; + } + + public string CreateSymlink(string target, params string[] symLink) + { + var allElements = new[] { FullPath }.Concat(symLink).ToArray(); + var symlinkPath = Path.Combine(allElements); + Directory.CreateSymbolicLink(symlinkPath, target); + return symlinkPath; + } + public void Dispose() { Directory.Delete(FullPath, true); From 8cd3a3072e220ad6f47e96c3adbd5abdc3f4c316 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Fri, 23 Dec 2022 00:07:33 +0100 Subject: [PATCH 58/92] refactor test --- Source/FileWatcherExTests/FileWatcherTest.cs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/Source/FileWatcherExTests/FileWatcherTest.cs b/Source/FileWatcherExTests/FileWatcherTest.cs index 875ee11..b77514a 100644 --- a/Source/FileWatcherExTests/FileWatcherTest.cs +++ b/Source/FileWatcherExTests/FileWatcherTest.cs @@ -41,20 +41,16 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() using var dir = new TempDir(); // {tempdir}/subdir1 - var subdirPath1 = Path.Combine(dir.FullPath, "subdir1"); - Directory.CreateDirectory(subdirPath1); + var subdirPath1 = dir.CreateSubDir("subdir1"); // {tempdir}/subdir2 - var subdirPath2 = Path.Combine(dir.FullPath, "subdir2"); - Directory.CreateDirectory(subdirPath2); + var subdirPath2 = dir.CreateSubDir("subdir2"); // symlink {tempdir}/sym1 to {tempdir}/subdir1 - var symlinkPath1 = Path.Combine(dir.FullPath, "sym1"); - Directory.CreateSymbolicLink(symlinkPath1, subdirPath1); + var symlinkPath1 = dir.CreateSymlink(symLink: "sym1", target: subdirPath1); // symlink {tempdir}/sym1/sym2 to {tempdir}/subdir2 - var symlinkPath2 = Path.Combine(dir.FullPath, "sym1", "sym2"); - Directory.CreateSymbolicLink(symlinkPath2, subdirPath2); + var symlinkPath2 = dir.CreateSymlink(symLink: new []{"sym1", "sym2"}, target: subdirPath2); _uut = CreateFileWatcher(dir.FullPath); @@ -69,9 +65,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() using var dir = new TempDir(); _uut = CreateFileWatcher(dir.FullPath); - // create subdir - var subdirPath = Path.Combine(dir.FullPath, "subdir"); - Directory.CreateDirectory(subdirPath); + var subdirPath = dir.CreateSubDir("subdir"); // simulate file watcher trigger _uut.TryRegisterFileWatcherForSymbolicLinkDir(subdirPath); @@ -80,9 +74,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() Assert.Single(_uut.FwDictionary); AssertContainsWatcherFor(dir.FullPath); - // create symlink - var symlinkPath = Path.Combine(dir.FullPath, "sym"); - Directory.CreateSymbolicLink(symlinkPath, subdirPath); + var symlinkPath = dir.CreateSymlink(symLink: "sym", target: subdirPath); // simulate file watcher trigger _uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath); From 4bc2f3272a8fab6f3a2edf55521bfa206237f50a Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Wed, 4 Jan 2023 13:28:49 +0100 Subject: [PATCH 59/92] update test --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 5 -- Source/FileWatcherEx/Helpers/FileWatcher.cs | 72 ++++++++++++-------- Source/FileWatcherExTests/FileWatcherTest.cs | 33 +++++++-- 3 files changed, 73 insertions(+), 37 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index f7ed9a4..ce62a12 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -278,16 +278,11 @@ void onError(ErrorEventArgs e) _fsw.Filters.Add(filter); } - // all _fsw.NotifyFilter = NotifyFilter; // all. if this is not enabled, then also no additional file watchers should be registered _fsw.IncludeSubdirectories = IncludeSubdirectories; - // exception: only root watcher _fsw.SynchronizingObject = SynchronizingObject; - - // global - // Start watching _fsw.EnableRaisingEvents = true; } diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index ecc8cce..eacbe45 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -2,6 +2,8 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +using System.ComponentModel; + namespace FileWatcherEx.Helpers; internal class FileWatcher : IDisposable @@ -14,7 +16,7 @@ internal class FileWatcher : IDisposable private Func? _getFileAttributesFunc; private Func? _getDirectoryInfosFunc; private Func _watcherFactory; - private Action _logger = _ => {}; + private Action _logger = _ => { }; // defaults from: // https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=net-7.0#property-value @@ -22,6 +24,9 @@ internal class FileWatcher : IDisposable | NotifyFilters.FileName | NotifyFilters.DirectoryName; + private bool _enableRaisingEvents; + private ISynchronizeInvoke _synchronizingObject; + internal Func GetFileAttributesFunc { get => _getFileAttributesFunc ?? File.GetAttributes; @@ -39,19 +44,31 @@ internal Func GetDirectoryInfosFunc } internal Dictionary FwDictionary => _fwDictionary; + public NotifyFilters NotifyFilter { set { _notifyFilters = value; - - foreach (var watcher in _fwDictionary.Values) - { - watcher.NotifyFilter = value; - } + _fwDictionary.Values.ToList().ForEach(w => w.NotifyFilter = value); } } + public bool EnableRaisingEvents + { + set + { + _enableRaisingEvents = value; + _fwDictionary.Values.ToList().ForEach(w => w.EnableRaisingEvents = value); + } + } + + public ISynchronizeInvoke SynchronizingObject + { + // only set root object + set => _fwDictionary[_watchPath].SynchronizingObject = value; + } + /// /// Create new instance of FileSystemWatcherWrapper /// @@ -60,37 +77,38 @@ public NotifyFilters NotifyFilter /// onError callback /// how to create a FileSystemWatcher /// logging callback - public FileWatcher(string path, Action onEvent, Action onError, Func watcherFactory, Action logger) + public FileWatcher(string path, Action onEvent, Action onError, + Func watcherFactory, Action logger) { _logger = logger; _watcherFactory = watcherFactory; _watchPath = path; - _eventCallback = onEvent; + _eventCallback = onEvent; _onError = onError; } - + public IFileSystemWatcherWrapper Init() { - var watcher = RegisterFileWatcher(_watchPath, enableRaisingEvents: false); + var watcher = RegisterFileWatcher(_watchPath); RegisterAdditionalFileWatchersForSymLinkDirs(_watchPath); return watcher; } - - private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRaisingEvents = true) + + private IFileSystemWatcherWrapper RegisterFileWatcher(string path) { var fileWatcher = _watcherFactory(); fileWatcher.Path = path; fileWatcher.NotifyFilter = _notifyFilters; fileWatcher.IncludeSubdirectories = true; - fileWatcher.EnableRaisingEvents = enableRaisingEvents; + fileWatcher.EnableRaisingEvents = _enableRaisingEvents; fileWatcher.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); fileWatcher.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); fileWatcher.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); fileWatcher.Renamed += (_, e) => ProcessRenamedEvent(e); fileWatcher.Error += (_, e) => _onError?.Invoke(e); - + // extra measures to handle symbolic link directories fileWatcher.Created += (_, e) => TryRegisterFileWatcherForSymbolicLinkDir(e.FullPath); fileWatcher.Deleted += UnregisterFileWatcherForSymbolicLinkDir; @@ -101,8 +119,8 @@ private IFileSystemWatcherWrapper RegisterFileWatcher(string path, bool enableRa _fwDictionary.Add(path, fileWatcher); return fileWatcher; } - - + + /// /// Recursively find sym link dir and register them. /// Background: the native filewatcher does not follow symlinks so they need to be treated separately. @@ -120,7 +138,7 @@ private void RegisterAdditionalFileWatchersForSymLinkDirs(string path) } } - + /// /// Process event for type = [CHANGED; DELETED; CREATED] /// @@ -132,8 +150,8 @@ private void ProcessEvent(FileSystemEventArgs e, ChangeType changeType) FullPath = e.FullPath, }); } - - + + private void ProcessRenamedEvent(RenamedEventArgs e) { _eventCallback?.Invoke(new() @@ -143,13 +161,13 @@ private void ProcessRenamedEvent(RenamedEventArgs e) OldFullPath = e.OldFullPath, }); } - - + + internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) { try { - if ( IsSymbolicLinkDirectory(path) && !_fwDictionary.ContainsKey(path) ) + if (IsSymbolicLinkDirectory(path) && !_fwDictionary.ContainsKey(path)) { RegisterFileWatcher(path); } @@ -162,7 +180,7 @@ internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) } } - + /// /// Cleanup filewatcher if a symbolic link dir is deleted /// @@ -174,8 +192,8 @@ internal void UnregisterFileWatcherForSymbolicLinkDir(object sender, FileSystemE _fwDictionary.Remove(e.FullPath); } } - - + + private bool IsSymbolicLinkDirectory(string path) { var attrs = GetFileAttributesFunc(path); @@ -188,8 +206,8 @@ internal List GetFileWatchers() { return _fwDictionary.Values.ToList(); } - - + + /// /// Dispose the instance /// diff --git a/Source/FileWatcherExTests/FileWatcherTest.cs b/Source/FileWatcherExTests/FileWatcherTest.cs index b77514a..1e4223b 100644 --- a/Source/FileWatcherExTests/FileWatcherTest.cs +++ b/Source/FileWatcherExTests/FileWatcherTest.cs @@ -1,4 +1,5 @@ -using FileWatcherEx; +using System.ComponentModel; +using FileWatcherEx; using FileWatcherEx.Helpers; using FileWatcherExTests.Helper; using Moq; @@ -10,7 +11,7 @@ namespace FileWatcherExTests; public class FileWatcherTest { private readonly ITestOutputHelper _testOutputHelper; - private FileWatcher _uut; + private FileWatcher? _uut; private readonly List> _mocks; public FileWatcherTest(ITestOutputHelper testOutputHelper) @@ -112,16 +113,21 @@ public void Properties_Are_Propagated() var subDir = dir.CreateSubDir("subdir"); - // symlink at startup detected + // symlink for detection at startup dir.CreateSymlink( symLink: "sym1", target: subDir); - // start the watcher + configure _uut = CreateFileWatcher(dir.FullPath); + + // perform settings. all, except SynchronizingObject are propagated + // to all registered watchers _uut.NotifyFilter = NotifyFilters.LastAccess; + _uut.EnableRaisingEvents = true; + var syncObj = new Mock().Object; + _uut.SynchronizingObject = syncObj; - // create symlink during runtime + // create symlink at runtime var symlinkPath2 = dir.CreateSymlink( symLink: "sym2", target: subDir); @@ -129,11 +135,28 @@ public void Properties_Are_Propagated() // simulate file watcher trigger _uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath2); + // 1x root watcher, 1x sym link at startup, 1x sym link at runtime Assert.Equal(3, _mocks.Count); + // all watchers have properties set Assert.All( _mocks, mock => mock.VerifySet(w => w.NotifyFilter = NotifyFilters.LastAccess)); + Assert.All( + _mocks, + mock => + mock.VerifySet(w => w.EnableRaisingEvents = true)); + + // sync. object is only set for root watcher + // TODO rename root watcher + Assert.Collection(_mocks, + mock => + { + mock.VerifySet(w => w.Path = dir.FullPath); + mock.VerifySet(w => w.SynchronizingObject = syncObj); + }, + mock => mock.VerifySet(w => w.SynchronizingObject = syncObj, Times.Never), + mock => mock.VerifySet(w => w.SynchronizingObject = syncObj, Times.Never)); } private FileWatcher CreateFileWatcher(string path) From 7fe63bf1b3e2528c48bd0059ba48e0553ced1177 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Wed, 4 Jan 2023 16:26:37 +0100 Subject: [PATCH 60/92] propagate Filters property --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 3 +- Source/FileWatcherEx/Helpers/FileWatcher.cs | 83 +++++++++++--------- Source/FileWatcherExTests/FileWatcherTest.cs | 27 +++++-- 3 files changed, 70 insertions(+), 43 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index ce62a12..489871c 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -272,14 +272,13 @@ void onError(ErrorEventArgs e) _watcher = new FileWatcher(FolderPath, onEvent, onError, FileSystemWatcherFactory, _ => {}); _fsw = _watcher.Init(); - // all foreach (var filter in Filters) { _fsw.Filters.Add(filter); } _fsw.NotifyFilter = NotifyFilter; - // all. if this is not enabled, then also no additional file watchers should be registered + // TODO all. if this is not enabled, then also no additional file watchers should be registered _fsw.IncludeSubdirectories = IncludeSubdirectories; _fsw.SynchronizingObject = SynchronizingObject; diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index eacbe45..800b355 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -2,6 +2,7 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ +using System.Collections.ObjectModel; using System.ComponentModel; namespace FileWatcherEx.Helpers; @@ -18,14 +19,6 @@ internal class FileWatcher : IDisposable private Func _watcherFactory; private Action _logger = _ => { }; - // defaults from: - // https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=net-7.0#property-value - private NotifyFilters _notifyFilters = NotifyFilters.LastWrite - | NotifyFilters.FileName - | NotifyFilters.DirectoryName; - - private bool _enableRaisingEvents; - private ISynchronizeInvoke _synchronizingObject; internal Func GetFileAttributesFunc { @@ -45,32 +38,24 @@ internal Func GetDirectoryInfosFunc internal Dictionary FwDictionary => _fwDictionary; - public NotifyFilters NotifyFilter - { - set - { - _notifyFilters = value; - _fwDictionary.Values.ToList().ForEach(w => w.NotifyFilter = value); - } - } - - public bool EnableRaisingEvents - { - set - { - _enableRaisingEvents = value; - _fwDictionary.Values.ToList().ForEach(w => w.EnableRaisingEvents = value); - } - } + // defaults from: + // https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=net-7.0#property-value + public NotifyFilters NotifyFilter { get; set; } = NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName; + public bool EnableRaisingEvents { get; set; } + + public Collection Filters { get; } = new(); - public ISynchronizeInvoke SynchronizingObject - { - // only set root object - set => _fwDictionary[_watchPath].SynchronizingObject = value; - } + public ISynchronizeInvoke SynchronizingObject { get; set; } /// /// Create new instance of FileSystemWatcherWrapper + /// + /// Object creation follows this order: + /// 1) create new instance + /// 2) set properties (optional) + /// 3) call init() (mandatory) /// /// Full folder path to watcher /// onEvent callback @@ -98,11 +83,15 @@ public IFileSystemWatcherWrapper Init() private IFileSystemWatcherWrapper RegisterFileWatcher(string path) { var fileWatcher = _watcherFactory(); - fileWatcher.Path = path; - fileWatcher.NotifyFilter = _notifyFilters; - fileWatcher.IncludeSubdirectories = true; - fileWatcher.EnableRaisingEvents = _enableRaisingEvents; + SetFileWatcherProperties(fileWatcher, path); + RegisterFileWatcherEventHandlers(fileWatcher); + + _fwDictionary.Add(path, fileWatcher); + return fileWatcher; + } + private void RegisterFileWatcherEventHandlers(IFileSystemWatcherWrapper fileWatcher) + { fileWatcher.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); fileWatcher.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); fileWatcher.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); @@ -112,12 +101,34 @@ private IFileSystemWatcherWrapper RegisterFileWatcher(string path) // extra measures to handle symbolic link directories fileWatcher.Created += (_, e) => TryRegisterFileWatcherForSymbolicLinkDir(e.FullPath); fileWatcher.Deleted += UnregisterFileWatcherForSymbolicLinkDir; + } + + private void SetFileWatcherProperties(IFileSystemWatcherWrapper fileWatcher, string path) + { + fileWatcher.Path = path; + fileWatcher.NotifyFilter = NotifyFilter; + fileWatcher.IncludeSubdirectories = true; + fileWatcher.EnableRaisingEvents = EnableRaisingEvents; + + foreach (var filter in Filters) + { + fileWatcher.Filters.Add(filter); + } + + // currently the sync object is only registered for the root file watcher. + // this preserves the old behaviour + if (IsRootPath(path)) + { + fileWatcher.SynchronizingObject = SynchronizingObject; + } //changing this to a higher value can lead into issues when watching UNC drives fileWatcher.InternalBufferSize = 32768; + } - _fwDictionary.Add(path, fileWatcher); - return fileWatcher; + private bool IsRootPath(string path) + { + return _watchPath == path; } diff --git a/Source/FileWatcherExTests/FileWatcherTest.cs b/Source/FileWatcherExTests/FileWatcherTest.cs index 1e4223b..3f5c651 100644 --- a/Source/FileWatcherExTests/FileWatcherTest.cs +++ b/Source/FileWatcherExTests/FileWatcherTest.cs @@ -1,7 +1,9 @@ -using System.ComponentModel; +using System.Collections.ObjectModel; +using System.ComponentModel; using FileWatcherEx; using FileWatcherEx.Helpers; using FileWatcherExTests.Helper; +using Microsoft.VisualBasic; using Moq; using Xunit; using Xunit.Abstractions; @@ -11,6 +13,7 @@ namespace FileWatcherExTests; public class FileWatcherTest { private readonly ITestOutputHelper _testOutputHelper; + // TODO can this be on a test level private FileWatcher? _uut; private readonly List> _mocks; @@ -118,21 +121,30 @@ public void Properties_Are_Propagated() symLink: "sym1", target: subDir); - _uut = CreateFileWatcher(dir.FullPath); - + _uut = new FileWatcher(dir.FullPath, + _ => { }, + _ => { }, + WatcherFactoryWithMemory, + _ => { }); + // perform settings. all, except SynchronizingObject are propagated // to all registered watchers _uut.NotifyFilter = NotifyFilters.LastAccess; + _uut.Filters.Add("*.foo"); + _uut.Filters.Add("*.bar"); _uut.EnableRaisingEvents = true; var syncObj = new Mock().Object; _uut.SynchronizingObject = syncObj; + + // finish object initialization + _uut.Init(); // create symlink at runtime var symlinkPath2 = dir.CreateSymlink( symLink: "sym2", target: subDir); - // simulate file watcher trigger + // simulate that a new symlink dir was added _uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath2); // 1x root watcher, 1x sym link at startup, 1x sym link at runtime @@ -146,7 +158,10 @@ public void Properties_Are_Propagated() _mocks, mock => mock.VerifySet(w => w.EnableRaisingEvents = true)); - + Assert.All( + _mocks, + mock => Assert.Equal(mock.Object.Filters, new Collection { "*.foo", "*.bar" })); + // sync. object is only set for root watcher // TODO rename root watcher Assert.Collection(_mocks, @@ -173,6 +188,8 @@ private FileWatcher CreateFileWatcher(string path) private IFileSystemWatcherWrapper WatcherFactoryWithMemory() { var mock = new Mock(); + // this did the trick to have the 'Filters' property be recorded + mock.SetReturnsDefault(new Collection()); _mocks.Add(mock); return mock.Object; } From c7338e25f925549549d8a4c08ede36e337e42028 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Wed, 4 Jan 2023 17:14:16 +0100 Subject: [PATCH 61/92] rename + simplify --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 37 +++++++++----------- Source/FileWatcherExTests/FileWatcherTest.cs | 19 +++++----- 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 800b355..854c0f9 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -10,14 +10,13 @@ namespace FileWatcherEx.Helpers; internal class FileWatcher : IDisposable { // TODO double check properties -> are they all needed ? - private string _watchPath = string.Empty; - private Action? _eventCallback = null; - private readonly Dictionary _fwDictionary = new(); - private Action? _onError = null; + private readonly string _watchPath; + private readonly Action? _eventCallback; + private readonly Action? _onError; private Func? _getFileAttributesFunc; private Func? _getDirectoryInfosFunc; - private Func _watcherFactory; - private Action _logger = _ => { }; + private readonly Func _watcherFactory; + private readonly Action _logger; internal Func GetFileAttributesFunc @@ -36,7 +35,7 @@ internal Func GetDirectoryInfosFunc set => _getDirectoryInfosFunc = value; } - internal Dictionary FwDictionary => _fwDictionary; + internal Dictionary FileWatchers { get; } = new(); // defaults from: // https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=net-7.0#property-value @@ -47,7 +46,7 @@ internal Func GetDirectoryInfosFunc public Collection Filters { get; } = new(); - public ISynchronizeInvoke SynchronizingObject { get; set; } + public ISynchronizeInvoke? SynchronizingObject { get; set; } /// /// Create new instance of FileSystemWatcherWrapper @@ -86,7 +85,7 @@ private IFileSystemWatcherWrapper RegisterFileWatcher(string path) SetFileWatcherProperties(fileWatcher, path); RegisterFileWatcherEventHandlers(fileWatcher); - _fwDictionary.Add(path, fileWatcher); + FileWatchers.Add(path, fileWatcher); return fileWatcher; } @@ -109,11 +108,7 @@ private void SetFileWatcherProperties(IFileSystemWatcherWrapper fileWatcher, str fileWatcher.NotifyFilter = NotifyFilter; fileWatcher.IncludeSubdirectories = true; fileWatcher.EnableRaisingEvents = EnableRaisingEvents; - - foreach (var filter in Filters) - { - fileWatcher.Filters.Add(filter); - } + Filters.ToList().ForEach(filter => fileWatcher.Filters.Add(filter)); // currently the sync object is only registered for the root file watcher. // this preserves the old behaviour @@ -178,7 +173,7 @@ internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) { try { - if (IsSymbolicLinkDirectory(path) && !_fwDictionary.ContainsKey(path)) + if (IsSymbolicLinkDirectory(path) && !FileWatchers.ContainsKey(path)) { RegisterFileWatcher(path); } @@ -197,10 +192,10 @@ internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) /// internal void UnregisterFileWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) { - if (_fwDictionary.ContainsKey(e.FullPath)) + if (FileWatchers.ContainsKey(e.FullPath)) { - _fwDictionary[e.FullPath].Dispose(); - _fwDictionary.Remove(e.FullPath); + FileWatchers[e.FullPath].Dispose(); + FileWatchers.Remove(e.FullPath); } } @@ -215,7 +210,7 @@ private bool IsSymbolicLinkDirectory(string path) // for testing internal List GetFileWatchers() { - return _fwDictionary.Values.ToList(); + return FileWatchers.Values.ToList(); } @@ -224,9 +219,9 @@ internal List GetFileWatchers() /// public void Dispose() { - foreach (var item in _fwDictionary) + foreach (var pair in FileWatchers) { - item.Value.Dispose(); + pair.Value.Dispose(); } } } \ No newline at end of file diff --git a/Source/FileWatcherExTests/FileWatcherTest.cs b/Source/FileWatcherExTests/FileWatcherTest.cs index 3f5c651..23a7687 100644 --- a/Source/FileWatcherExTests/FileWatcherTest.cs +++ b/Source/FileWatcherExTests/FileWatcherTest.cs @@ -75,7 +75,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() _uut.TryRegisterFileWatcherForSymbolicLinkDir(subdirPath); // subdir is ignored - Assert.Single(_uut.FwDictionary); + Assert.Single(_uut.FileWatchers); AssertContainsWatcherFor(dir.FullPath); var symlinkPath = dir.CreateSymlink(symLink: "sym", target: subdirPath); @@ -84,7 +84,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() _uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath); // symlink dir is registered - Assert.Equal(2, _uut.FwDictionary.Count); + Assert.Equal(2, _uut.FileWatchers.Count); AssertContainsWatcherFor(dir.FullPath); AssertContainsWatcherFor(symlinkPath); @@ -96,7 +96,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() new FileSystemEventArgs(WatcherChangeTypes.Deleted, dir.FullPath, "sym")); // sym-link file watcher is removed - Assert.Single(_uut.FwDictionary); + Assert.Single(_uut.FileWatchers); AssertContainsWatcherFor(dir.FullPath); _uut.Dispose(); @@ -163,15 +163,14 @@ public void Properties_Are_Propagated() mock => Assert.Equal(mock.Object.Filters, new Collection { "*.foo", "*.bar" })); // sync. object is only set for root watcher - // TODO rename root watcher Assert.Collection(_mocks, - mock => + rootWatcherMock => { - mock.VerifySet(w => w.Path = dir.FullPath); - mock.VerifySet(w => w.SynchronizingObject = syncObj); + rootWatcherMock.VerifySet(w => w.Path = dir.FullPath); + rootWatcherMock.VerifySet(w => w.SynchronizingObject = syncObj); }, - mock => mock.VerifySet(w => w.SynchronizingObject = syncObj, Times.Never), - mock => mock.VerifySet(w => w.SynchronizingObject = syncObj, Times.Never)); + otherWatcherMock => otherWatcherMock.VerifySet(w => w.SynchronizingObject = syncObj, Times.Never), + otherWatcherMock => otherWatcherMock.VerifySet(w => w.SynchronizingObject = syncObj, Times.Never)); } private FileWatcher CreateFileWatcher(string path) @@ -196,7 +195,7 @@ private IFileSystemWatcherWrapper WatcherFactoryWithMemory() private void AssertContainsWatcherFor(string path) { - var _ = _uut.FwDictionary[path]; + var _ = _uut.FileWatchers[path]; var foundMocks = ( from mock in _mocks where HasPropertySetTo(mock, watcher => watcher.Path = path) From 8b83e9ffddf3a7d5189242bcd58e27b55c548eb9 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Wed, 4 Jan 2023 17:28:31 +0100 Subject: [PATCH 62/92] propagate IncludeSubdirectories --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 2 -- Source/FileWatcherEx/Helpers/FileWatcher.cs | 7 +++++-- Source/FileWatcherExTests/FileWatcherTest.cs | 5 +++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 489871c..f2f307d 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -278,9 +278,7 @@ void onError(ErrorEventArgs e) } _fsw.NotifyFilter = NotifyFilter; - // TODO all. if this is not enabled, then also no additional file watchers should be registered _fsw.IncludeSubdirectories = IncludeSubdirectories; - _fsw.SynchronizingObject = SynchronizingObject; _fsw.EnableRaisingEvents = true; } diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 854c0f9..bd29ec4 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -44,10 +44,13 @@ internal Func GetDirectoryInfosFunc | NotifyFilters.DirectoryName; public bool EnableRaisingEvents { get; set; } + public bool IncludeSubdirectories { get; set; } + public Collection Filters { get; } = new(); public ISynchronizeInvoke? SynchronizingObject { get; set; } - + + /// /// Create new instance of FileSystemWatcherWrapper /// @@ -106,7 +109,7 @@ private void SetFileWatcherProperties(IFileSystemWatcherWrapper fileWatcher, str { fileWatcher.Path = path; fileWatcher.NotifyFilter = NotifyFilter; - fileWatcher.IncludeSubdirectories = true; + fileWatcher.IncludeSubdirectories = IncludeSubdirectories; fileWatcher.EnableRaisingEvents = EnableRaisingEvents; Filters.ToList().ForEach(filter => fileWatcher.Filters.Add(filter)); diff --git a/Source/FileWatcherExTests/FileWatcherTest.cs b/Source/FileWatcherExTests/FileWatcherTest.cs index 23a7687..9f33e89 100644 --- a/Source/FileWatcherExTests/FileWatcherTest.cs +++ b/Source/FileWatcherExTests/FileWatcherTest.cs @@ -133,6 +133,7 @@ public void Properties_Are_Propagated() _uut.Filters.Add("*.foo"); _uut.Filters.Add("*.bar"); _uut.EnableRaisingEvents = true; + _uut.IncludeSubdirectories = true; var syncObj = new Mock().Object; _uut.SynchronizingObject = syncObj; @@ -158,6 +159,10 @@ public void Properties_Are_Propagated() _mocks, mock => mock.VerifySet(w => w.EnableRaisingEvents = true)); + Assert.All( + _mocks, + mock => + mock.VerifySet(w => w.IncludeSubdirectories = true)); Assert.All( _mocks, mock => Assert.Equal(mock.Object.Filters, new Collection { "*.foo", "*.bar" })); From 709d06e9f372d0db7f080ba7b65cc317001488c9 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Wed, 4 Jan 2023 17:47:24 +0100 Subject: [PATCH 63/92] add logging --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 22 ++++++++++----------- Source/FileWatcherEx/Helpers/FileWatcher.cs | 10 +++++----- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index f2f307d..04acb26 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -269,18 +269,18 @@ void onError(ErrorEventArgs e) // - `watcher` should delegate the set properties to ALL file watchers // - if you register a dir later, these 4 properties should be used // Start watcher - _watcher = new FileWatcher(FolderPath, onEvent, onError, FileSystemWatcherFactory, _ => {}); - _fsw = _watcher.Init(); - - foreach (var filter in Filters) + // _watcher = new FileWatcher(FolderPath, onEvent, onError, FileSystemWatcherFactory, _ => {}); + + // TODO do some manual testing + _watcher = new FileWatcher(FolderPath, onEvent, onError, FileSystemWatcherFactory, Console.WriteLine) { - _fsw.Filters.Add(filter); - } - - _fsw.NotifyFilter = NotifyFilter; - _fsw.IncludeSubdirectories = IncludeSubdirectories; - _fsw.SynchronizingObject = SynchronizingObject; - _fsw.EnableRaisingEvents = true; + NotifyFilter = NotifyFilter, + IncludeSubdirectories = IncludeSubdirectories, + SynchronizingObject = SynchronizingObject, + EnableRaisingEvents = true + }; + Filters.ToList().ForEach(filter => _watcher.Filters.Add(filter)); + _watcher.Init(); } internal void StartForTesting( diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index bd29ec4..3e6dff8 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -74,22 +74,21 @@ public FileWatcher(string path, Action onEvent, Action Date: Thu, 5 Jan 2023 09:20:15 +0100 Subject: [PATCH 64/92] manual test --- Source/Demo/Form1.cs | 20 +++++++++++---- Source/Demo/README.md | 9 +++++++ Source/FileWatcherEx/FileSystemWatcherEx.cs | 23 ++++++----------- Source/FileWatcherEx/Helpers/FileWatcher.cs | 28 +++++++++++---------- 4 files changed, 46 insertions(+), 34 deletions(-) create mode 100644 Source/Demo/README.md diff --git a/Source/Demo/Form1.cs b/Source/Demo/Form1.cs index ba1b0f0..07649b6 100644 --- a/Source/Demo/Form1.cs +++ b/Source/Demo/Form1.cs @@ -16,7 +16,7 @@ public Form1() private void BtnStart_Click(object sender, EventArgs e) { - _fw = new FileSystemWatcherEx(txtPath.Text.Trim()); + _fw = new FileSystemWatcherEx(txtPath.Text.Trim(), FW_OnLog); _fw.OnRenamed += FW_OnRenamed; _fw.OnCreated += FW_OnCreated; @@ -76,8 +76,11 @@ private void FW_OnRenamed(object? sender, FileChangedEvent e) e.FullPath) + "\r\n"; } - - + private void FW_OnLog(string value) + { + txtConsole.Text += $@"[log] {value}" + "\r\n";; + } + private void BtnStop_Click(object sender, EventArgs e) { _fw.Stop(); @@ -105,8 +108,15 @@ private void BtnSelectFolder_Click(object sender, EventArgs e) private void Form1_FormClosing(object sender, FormClosingEventArgs e) { - _fw.Stop(); - _fw.Dispose(); + try + { + _fw.Stop(); + _fw.Dispose(); + } + catch + { + // intentionally empty + } } } } \ No newline at end of file diff --git a/Source/Demo/README.md b/Source/Demo/README.md new file mode 100644 index 0000000..bff6a63 --- /dev/null +++ b/Source/Demo/README.md @@ -0,0 +1,9 @@ +Manual Testing +-------------- + +This little application is helpful for interactive testing of the library. + +Symlinks +-------- +To create a symlink directory on Windows, use `mklink`. +Example: `mklink /D my-symbolic-link c:\temp\target-directory` diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 04acb26..7ce9261 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -20,6 +20,7 @@ public class FileSystemWatcherEx : IDisposable, IFileSystemWatcherEx private FileWatcher? _watcher; private IFileSystemWatcherWrapper? _fsw; private Func? _fswFactory; + private readonly Action _logger; // Define the cancellation token. private CancellationTokenSource? _cancelSource; @@ -134,12 +135,13 @@ public string Filter /// Initialize new instance of /// /// - public FileSystemWatcherEx(string folderPath = "") + /// Optional Action to log out library internals + public FileSystemWatcherEx(string folderPath = "", Action? logger = null) { FolderPath = folderPath; + _logger = logger ?? (_ => {}) ; } - - + /// /// Start watching files /// @@ -262,17 +264,7 @@ void onError(ErrorEventArgs e) } } - - // TODO - // - FileWatcher should not return underlying filewatcher - // - introduce stronger encapsulation - // - `watcher` should delegate the set properties to ALL file watchers - // - if you register a dir later, these 4 properties should be used - // Start watcher - // _watcher = new FileWatcher(FolderPath, onEvent, onError, FileSystemWatcherFactory, _ => {}); - - // TODO do some manual testing - _watcher = new FileWatcher(FolderPath, onEvent, onError, FileSystemWatcherFactory, Console.WriteLine) + _watcher = new FileWatcher(FolderPath, onEvent, onError, FileSystemWatcherFactory, _logger) { NotifyFilter = NotifyFilter, IncludeSubdirectories = IncludeSubdirectories, @@ -344,6 +336,5 @@ private void Thread_DoingWork(CancellationToken cancelToken) } } } - - } + diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 3e6dff8..c22c9bd 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -80,6 +80,8 @@ public void Init() RegisterAdditionalFileWatchersForSymLinkDirs(_watchPath); } + // TODO when no sub dirs are watched, also no sym links are watched + // TODO build warnings private void RegisterFileWatcher(string path) { @@ -91,19 +93,6 @@ private void RegisterFileWatcher(string path) FileWatchers.Add(path, fileWatcher); } - private void RegisterFileWatcherEventHandlers(IFileSystemWatcherWrapper fileWatcher) - { - fileWatcher.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); - fileWatcher.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); - fileWatcher.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); - fileWatcher.Renamed += (_, e) => ProcessRenamedEvent(e); - fileWatcher.Error += (_, e) => _onError?.Invoke(e); - - // extra measures to handle symbolic link directories - fileWatcher.Created += (_, e) => TryRegisterFileWatcherForSymbolicLinkDir(e.FullPath); - fileWatcher.Deleted += UnregisterFileWatcherForSymbolicLinkDir; - } - private void SetFileWatcherProperties(IFileSystemWatcherWrapper fileWatcher, string path) { fileWatcher.Path = path; @@ -123,6 +112,19 @@ private void SetFileWatcherProperties(IFileSystemWatcherWrapper fileWatcher, str fileWatcher.InternalBufferSize = 32768; } + private void RegisterFileWatcherEventHandlers(IFileSystemWatcherWrapper fileWatcher) + { + fileWatcher.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); + fileWatcher.Changed += (_, e) => ProcessEvent(e, ChangeType.CHANGED); + fileWatcher.Deleted += (_, e) => ProcessEvent(e, ChangeType.DELETED); + fileWatcher.Renamed += (_, e) => ProcessRenamedEvent(e); + fileWatcher.Error += (_, e) => _onError?.Invoke(e); + + // extra measures to handle symbolic link directories + fileWatcher.Created += (_, e) => TryRegisterFileWatcherForSymbolicLinkDir(e.FullPath); + fileWatcher.Deleted += UnregisterFileWatcherForSymbolicLinkDir; + } + private bool IsRootPath(string path) { return _watchPath == path; From 47218593b22364974feeff6876d580520a0c3a6e Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 5 Jan 2023 14:03:34 +0100 Subject: [PATCH 65/92] fix integration test --- .../FileWatcherExIntegrationTest.cs | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index ce34c89..3c9fc00 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -11,19 +11,19 @@ namespace FileWatcherExTests; /// public class FileWatcherExIntegrationTest : IDisposable { - private ConcurrentQueue _events; - private ReplayFileSystemWatcherWrapper _replayer; - private FileSystemWatcherEx _fileWatcher; + private readonly ConcurrentQueue _events; + private readonly FileSystemWatcherEx _fileWatcher; + private readonly ReplayFileSystemWatcherFactory _replayFileSystemWatcherFactory; public FileWatcherExIntegrationTest() { // setup before each test run - _events = new(); - _replayer = new(); + _events = new ConcurrentQueue(); + _replayFileSystemWatcherFactory = new ReplayFileSystemWatcherFactory(); const string recordingDir = @"C:\temp\fwtest"; _fileWatcher = new FileSystemWatcherEx(recordingDir); - _fileWatcher.FileSystemWatcherFactory = () => _replayer; + _fileWatcher.FileSystemWatcherFactory = () => _replayFileSystemWatcherFactory.Create(); _fileWatcher.IncludeSubdirectories = true; _fileWatcher.OnCreated += (_, ev) => _events.Enqueue(ev); @@ -338,8 +338,23 @@ private void StartFileWatcherAndReplay(string csvFile) p => FileAttributes.Normal, // only used for FullName p => new[] { new DirectoryInfo(p)}); - _replayer.Replay(csvFile); + _replayFileSystemWatcherFactory.RootWatcher.Replay(csvFile); _fileWatcher.Stop(); } + + private class ReplayFileSystemWatcherFactory + { + private readonly List _wrappers = new(); + + public ReplayFileSystemWatcherWrapper Create() + { + var watcher = new ReplayFileSystemWatcherWrapper(); + _wrappers.Add(watcher); + return watcher; + } + // At integration test, we're only interested in the root file watcher. + // This is the one which is registered first and watches the root directory. + public ReplayFileSystemWatcherWrapper RootWatcher => _wrappers[0]; + } } \ No newline at end of file From 4f6dd85ccd772ab5ee4e58c65162262f12e01c14 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 5 Jan 2023 14:34:30 +0100 Subject: [PATCH 66/92] clear warnings --- Source/Demo/Form1.cs | 10 +--------- Source/FileWatcherEx/FileSystemWatcherEx.cs | 8 -------- Source/FileWatcherEx/Helpers/FileWatcher.cs | 9 ++++----- Source/FileWatcherExTests/FileWatcherTest.cs | 2 +- .../ReplayFileSystemWatcherWrapper.cs | 4 +++- 5 files changed, 9 insertions(+), 24 deletions(-) diff --git a/Source/Demo/Form1.cs b/Source/Demo/Form1.cs index 07649b6..c5a739c 100644 --- a/Source/Demo/Form1.cs +++ b/Source/Demo/Form1.cs @@ -108,15 +108,7 @@ private void BtnSelectFolder_Click(object sender, EventArgs e) private void Form1_FormClosing(object sender, FormClosingEventArgs e) { - try - { - _fw.Stop(); - _fw.Dispose(); - } - catch - { - // intentionally empty - } + _fw.Dispose(); } } } \ No newline at end of file diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 7ce9261..e092991 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -18,7 +18,6 @@ public class FileSystemWatcherEx : IDisposable, IFileSystemWatcherEx private readonly BlockingCollection _fileEventQueue = new(); private FileWatcher? _watcher; - private IFileSystemWatcherWrapper? _fsw; private Func? _fswFactory; private readonly Action _logger; @@ -291,12 +290,6 @@ internal void StartForTesting( /// public void Stop() { - if (_fsw != null) - { - _fsw.EnableRaisingEvents = false; - _fsw.Dispose(); - } - _watcher?.Dispose(); // stop the thread @@ -310,7 +303,6 @@ public void Stop() /// public void Dispose() { - _fsw?.Dispose(); _watcher?.Dispose(); _cancelSource?.Dispose(); GC.SuppressFinalize(this); diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index c22c9bd..5bd544c 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -9,7 +9,6 @@ namespace FileWatcherEx.Helpers; internal class FileWatcher : IDisposable { - // TODO double check properties -> are they all needed ? private readonly string _watchPath; private readonly Action? _eventCallback; private readonly Action? _onError; @@ -81,7 +80,6 @@ public void Init() } // TODO when no sub dirs are watched, also no sym links are watched - // TODO build warnings private void RegisterFileWatcher(string path) { @@ -195,7 +193,7 @@ internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) /// /// Cleanup filewatcher if a symbolic link dir is deleted /// - internal void UnregisterFileWatcherForSymbolicLinkDir(object sender, FileSystemEventArgs e) + internal void UnregisterFileWatcherForSymbolicLinkDir(object? _, FileSystemEventArgs e) { if (FileWatchers.ContainsKey(e.FullPath)) { @@ -224,9 +222,10 @@ internal List GetFileWatchers() /// public void Dispose() { - foreach (var pair in FileWatchers) + foreach (var watcher in FileWatchers.Select(pair => pair.Value)) { - pair.Value.Dispose(); + watcher.EnableRaisingEvents = false; + watcher.Dispose(); } } } \ No newline at end of file diff --git a/Source/FileWatcherExTests/FileWatcherTest.cs b/Source/FileWatcherExTests/FileWatcherTest.cs index 9f33e89..e59466c 100644 --- a/Source/FileWatcherExTests/FileWatcherTest.cs +++ b/Source/FileWatcherExTests/FileWatcherTest.cs @@ -200,7 +200,7 @@ private IFileSystemWatcherWrapper WatcherFactoryWithMemory() private void AssertContainsWatcherFor(string path) { - var _ = _uut.FileWatchers[path]; + var _ = _uut?.FileWatchers[path]; var foundMocks = ( from mock in _mocks where HasPropertySetTo(mock, watcher => watcher.Path = path) diff --git a/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs b/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs index 653eba9..09a5c42 100644 --- a/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs +++ b/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs @@ -72,7 +72,7 @@ public void Replay(string csvFile) public event FileSystemEventHandler? Changed; public event RenamedEventHandler? Renamed; - // unused in replay implementation + #pragma warning disable CS8618 // unused in replay implementation public string Path { get; set; } public Collection Filters => _filters; @@ -80,6 +80,8 @@ public void Replay(string csvFile) public bool IncludeSubdirectories { get; set; } public bool EnableRaisingEvents { get; set; } public NotifyFilters NotifyFilter { get; set; } + + #pragma warning disable CS0067 // unused in replay implementation public event ErrorEventHandler? Error; public int InternalBufferSize { get; set; } public ISynchronizeInvoke? SynchronizingObject { get; set; } From 6799b16bb14ee43e0af8e79a2e5cab89b2a384f0 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 5 Jan 2023 14:42:42 +0100 Subject: [PATCH 67/92] symlink watchers are only registered if IncludeSubdirectories --- Source/FileWatcherEx/Helpers/FileWatcher.cs | 2 +- Source/FileWatcherExTests/FileWatcherTest.cs | 36 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/FileWatcher.cs index 5bd544c..e8ce8bd 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/FileWatcher.cs @@ -175,7 +175,7 @@ internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) { try { - if (IsSymbolicLinkDirectory(path) && !FileWatchers.ContainsKey(path)) + if (IsSymbolicLinkDirectory(path) && IncludeSubdirectories && !FileWatchers.ContainsKey(path)) { _logger($"Directory {path} is a symbolic link dir. Will register additional file watcher."); RegisterFileWatcher(path); diff --git a/Source/FileWatcherExTests/FileWatcherTest.cs b/Source/FileWatcherExTests/FileWatcherTest.cs index e59466c..3c1cac8 100644 --- a/Source/FileWatcherExTests/FileWatcherTest.cs +++ b/Source/FileWatcherExTests/FileWatcherTest.cs @@ -178,6 +178,41 @@ public void Properties_Are_Propagated() otherWatcherMock => otherWatcherMock.VerifySet(w => w.SynchronizingObject = syncObj, Times.Never)); } + + [Fact] + public void When_No_SubDirs_Are_Watched_Also_No_Additional_Symlink_Watchers_Are_Registered() + { + using var dir = new TempDir(); + + var subDir = dir.CreateSubDir("subdir"); + + // symlink for detection at startup + dir.CreateSymlink( + symLink: "sym1", + target: subDir); + + _uut = new FileWatcher(dir.FullPath, + _ => { }, + _ => { }, + WatcherFactoryWithMemory, + _ => { }); + + _uut.IncludeSubdirectories = false; + _uut.Init(); + + // create symlink at runtime + var symlinkPath2 = dir.CreateSymlink( + symLink: "sym2", + target: subDir); + + // simulate that a new symlink dir was added + _uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath2); + + // only root watcher was registered + Assert.Single(_mocks); + } + + private FileWatcher CreateFileWatcher(string path) { var fw = new FileWatcher(path, @@ -185,6 +220,7 @@ private FileWatcher CreateFileWatcher(string path) _ => { }, WatcherFactoryWithMemory, _ => { }); + fw.IncludeSubdirectories = true; fw.Init(); return fw; } From d371e8f3e1aa7432e388eaa6035c61cde5194c74 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 5 Jan 2023 14:45:42 +0100 Subject: [PATCH 68/92] some IDE refactorings --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index e092991..69b4c02 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -234,10 +234,10 @@ void InvokeRenamedEvent(object? sender, FileChangedEvent fileEvent) } }, (log) => { - Console.WriteLine(string.Format("{0} | {1}", Enum.GetName(typeof(ChangeType), ChangeType.LOG), log)); + Console.WriteLine($"{Enum.GetName(typeof(ChangeType), ChangeType.LOG)} | {log}"); }); - _cancelSource = new(); + _cancelSource = new CancellationTokenSource(); _thread = new Thread(() => Thread_DoingWork(_cancelSource.Token)) { // this ensures the thread does not block the process from terminating! @@ -248,22 +248,21 @@ void InvokeRenamedEvent(object? sender, FileChangedEvent fileEvent) // Log each event in our special format to output queue - void onEvent(FileChangedEvent e) + void OnEvent(FileChangedEvent e) { _fileEventQueue.Add(e); } - // OnError - void onError(ErrorEventArgs e) + void OnError(ErrorEventArgs e) { if (e != null) { - OnError?.Invoke(this, e); + this.OnError?.Invoke(this, e); } } - _watcher = new FileWatcher(FolderPath, onEvent, onError, FileSystemWatcherFactory, _logger) + _watcher = new FileWatcher(FolderPath, OnEvent, OnError, FileSystemWatcherFactory, _logger) { NotifyFilter = NotifyFilter, IncludeSubdirectories = IncludeSubdirectories, From c52b9d98bafc264aa68e4d10b51ebff845c2137b Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 5 Jan 2023 14:50:03 +0100 Subject: [PATCH 69/92] rename --- Source/FileWatcherEx/FileSystemWatcherEx.cs | 4 ++-- ...{FileWatcher.cs => SymlinkAwareFileWatcher.cs} | 6 ++---- ...cherTest.cs => SymlinkAwareFileWatcherTest.cs} | 15 +++++++-------- 3 files changed, 11 insertions(+), 14 deletions(-) rename Source/FileWatcherEx/Helpers/{FileWatcher.cs => SymlinkAwareFileWatcher.cs} (97%) rename Source/FileWatcherExTests/{FileWatcherTest.cs => SymlinkAwareFileWatcherTest.cs} (95%) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 69b4c02..d052f79 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -17,7 +17,7 @@ public class FileSystemWatcherEx : IDisposable, IFileSystemWatcherEx private EventProcessor? _processor; private readonly BlockingCollection _fileEventQueue = new(); - private FileWatcher? _watcher; + private SymlinkAwareFileWatcher? _watcher; private Func? _fswFactory; private readonly Action _logger; @@ -262,7 +262,7 @@ void OnError(ErrorEventArgs e) } } - _watcher = new FileWatcher(FolderPath, OnEvent, OnError, FileSystemWatcherFactory, _logger) + _watcher = new SymlinkAwareFileWatcher(FolderPath, OnEvent, OnError, FileSystemWatcherFactory, _logger) { NotifyFilter = NotifyFilter, IncludeSubdirectories = IncludeSubdirectories, diff --git a/Source/FileWatcherEx/Helpers/FileWatcher.cs b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs similarity index 97% rename from Source/FileWatcherEx/Helpers/FileWatcher.cs rename to Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs index e8ce8bd..d24c610 100644 --- a/Source/FileWatcherEx/Helpers/FileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs @@ -7,7 +7,7 @@ namespace FileWatcherEx.Helpers; -internal class FileWatcher : IDisposable +internal class SymlinkAwareFileWatcher : IDisposable { private readonly string _watchPath; private readonly Action? _eventCallback; @@ -63,7 +63,7 @@ internal Func GetDirectoryInfosFunc /// onError callback /// how to create a FileSystemWatcher /// logging callback - public FileWatcher(string path, Action onEvent, Action onError, + public SymlinkAwareFileWatcher(string path, Action onEvent, Action onError, Func watcherFactory, Action logger) { _logger = logger; @@ -79,8 +79,6 @@ public void Init() RegisterAdditionalFileWatchersForSymLinkDirs(_watchPath); } - // TODO when no sub dirs are watched, also no sym links are watched - private void RegisterFileWatcher(string path) { _logger($"Registering file watcher for {path}"); diff --git a/Source/FileWatcherExTests/FileWatcherTest.cs b/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs similarity index 95% rename from Source/FileWatcherExTests/FileWatcherTest.cs rename to Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs index 3c1cac8..34d5a3d 100644 --- a/Source/FileWatcherExTests/FileWatcherTest.cs +++ b/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs @@ -3,21 +3,20 @@ using FileWatcherEx; using FileWatcherEx.Helpers; using FileWatcherExTests.Helper; -using Microsoft.VisualBasic; using Moq; using Xunit; using Xunit.Abstractions; namespace FileWatcherExTests; -public class FileWatcherTest +public class SymlinkAwareFileWatcherTest { private readonly ITestOutputHelper _testOutputHelper; // TODO can this be on a test level - private FileWatcher? _uut; + private SymlinkAwareFileWatcher? _uut; private readonly List> _mocks; - public FileWatcherTest(ITestOutputHelper testOutputHelper) + public SymlinkAwareFileWatcherTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; _mocks = new List>(); @@ -121,7 +120,7 @@ public void Properties_Are_Propagated() symLink: "sym1", target: subDir); - _uut = new FileWatcher(dir.FullPath, + _uut = new SymlinkAwareFileWatcher(dir.FullPath, _ => { }, _ => { }, WatcherFactoryWithMemory, @@ -191,7 +190,7 @@ public void When_No_SubDirs_Are_Watched_Also_No_Additional_Symlink_Watchers_Are_ symLink: "sym1", target: subDir); - _uut = new FileWatcher(dir.FullPath, + _uut = new SymlinkAwareFileWatcher(dir.FullPath, _ => { }, _ => { }, WatcherFactoryWithMemory, @@ -213,9 +212,9 @@ public void When_No_SubDirs_Are_Watched_Also_No_Additional_Symlink_Watchers_Are_ } - private FileWatcher CreateFileWatcher(string path) + private SymlinkAwareFileWatcher CreateFileWatcher(string path) { - var fw = new FileWatcher(path, + var fw = new SymlinkAwareFileWatcher(path, _ => { }, _ => { }, WatcherFactoryWithMemory, From fbbf059b70cc9096a7e5f0e74da52b0836650487 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 5 Jan 2023 14:57:55 +0100 Subject: [PATCH 70/92] simplify test --- .../SymlinkAwareFileWatcherTest.cs | 55 +++++++++---------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs b/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs index 34d5a3d..55ac909 100644 --- a/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs +++ b/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs @@ -12,8 +12,6 @@ namespace FileWatcherExTests; public class SymlinkAwareFileWatcherTest { private readonly ITestOutputHelper _testOutputHelper; - // TODO can this be on a test level - private SymlinkAwareFileWatcher? _uut; private readonly List> _mocks; public SymlinkAwareFileWatcherTest(ITestOutputHelper testOutputHelper) @@ -26,7 +24,7 @@ public SymlinkAwareFileWatcherTest(ITestOutputHelper testOutputHelper) public void Root_Watcher_Is_Created() { using var dir = new TempDir(); - _uut = CreateFileWatcher(dir.FullPath); + CreateFileWatcher(dir.FullPath); AssertContainsWatcherFor(dir.FullPath); } @@ -55,7 +53,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() // symlink {tempdir}/sym1/sym2 to {tempdir}/subdir2 var symlinkPath2 = dir.CreateSymlink(symLink: new []{"sym1", "sym2"}, target: subdirPath2); - _uut = CreateFileWatcher(dir.FullPath); + CreateFileWatcher(dir.FullPath); AssertContainsWatcherFor(dir.FullPath); AssertContainsWatcherFor(symlinkPath1); @@ -66,24 +64,24 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() { using var dir = new TempDir(); - _uut = CreateFileWatcher(dir.FullPath); + var uut = CreateFileWatcher(dir.FullPath); var subdirPath = dir.CreateSubDir("subdir"); // simulate file watcher trigger - _uut.TryRegisterFileWatcherForSymbolicLinkDir(subdirPath); + uut.TryRegisterFileWatcherForSymbolicLinkDir(subdirPath); // subdir is ignored - Assert.Single(_uut.FileWatchers); + Assert.Single(uut.FileWatchers); AssertContainsWatcherFor(dir.FullPath); var symlinkPath = dir.CreateSymlink(symLink: "sym", target: subdirPath); // simulate file watcher trigger - _uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath); + uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath); // symlink dir is registered - Assert.Equal(2, _uut.FileWatchers.Count); + Assert.Equal(2, uut.FileWatchers.Count); AssertContainsWatcherFor(dir.FullPath); AssertContainsWatcherFor(symlinkPath); @@ -91,21 +89,21 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_During_Runtime() Directory.Delete(symlinkPath); // simulate file watcher trigger - _uut.UnregisterFileWatcherForSymbolicLinkDir(null, + uut.UnregisterFileWatcherForSymbolicLinkDir(null, new FileSystemEventArgs(WatcherChangeTypes.Deleted, dir.FullPath, "sym")); // sym-link file watcher is removed - Assert.Single(_uut.FileWatchers); + Assert.Single(uut.FileWatchers); AssertContainsWatcherFor(dir.FullPath); - _uut.Dispose(); + uut.Dispose(); } [Fact] public void MakeWatcher_Create_Exceptions_Are_Silently_Ignored() { - _uut = CreateFileWatcher("/bar"); - _uut.TryRegisterFileWatcherForSymbolicLinkDir("/not/existing/foo"); + var uut = CreateFileWatcher("/bar"); + uut.TryRegisterFileWatcherForSymbolicLinkDir("/not/existing/foo"); } [Fact] @@ -120,7 +118,7 @@ public void Properties_Are_Propagated() symLink: "sym1", target: subDir); - _uut = new SymlinkAwareFileWatcher(dir.FullPath, + var uut = new SymlinkAwareFileWatcher(dir.FullPath, _ => { }, _ => { }, WatcherFactoryWithMemory, @@ -128,16 +126,16 @@ public void Properties_Are_Propagated() // perform settings. all, except SynchronizingObject are propagated // to all registered watchers - _uut.NotifyFilter = NotifyFilters.LastAccess; - _uut.Filters.Add("*.foo"); - _uut.Filters.Add("*.bar"); - _uut.EnableRaisingEvents = true; - _uut.IncludeSubdirectories = true; + uut.NotifyFilter = NotifyFilters.LastAccess; + uut.Filters.Add("*.foo"); + uut.Filters.Add("*.bar"); + uut.EnableRaisingEvents = true; + uut.IncludeSubdirectories = true; var syncObj = new Mock().Object; - _uut.SynchronizingObject = syncObj; + uut.SynchronizingObject = syncObj; // finish object initialization - _uut.Init(); + uut.Init(); // create symlink at runtime var symlinkPath2 = dir.CreateSymlink( @@ -145,7 +143,7 @@ public void Properties_Are_Propagated() target: subDir); // simulate that a new symlink dir was added - _uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath2); + uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath2); // 1x root watcher, 1x sym link at startup, 1x sym link at runtime Assert.Equal(3, _mocks.Count); @@ -178,7 +176,7 @@ public void Properties_Are_Propagated() } - [Fact] + [Fact] public void When_No_SubDirs_Are_Watched_Also_No_Additional_Symlink_Watchers_Are_Registered() { using var dir = new TempDir(); @@ -190,14 +188,14 @@ public void When_No_SubDirs_Are_Watched_Also_No_Additional_Symlink_Watchers_Are_ symLink: "sym1", target: subDir); - _uut = new SymlinkAwareFileWatcher(dir.FullPath, + var uut = new SymlinkAwareFileWatcher(dir.FullPath, _ => { }, _ => { }, WatcherFactoryWithMemory, _ => { }); - _uut.IncludeSubdirectories = false; - _uut.Init(); + uut.IncludeSubdirectories = false; + uut.Init(); // create symlink at runtime var symlinkPath2 = dir.CreateSymlink( @@ -205,7 +203,7 @@ public void When_No_SubDirs_Are_Watched_Also_No_Additional_Symlink_Watchers_Are_ target: subDir); // simulate that a new symlink dir was added - _uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath2); + uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath2); // only root watcher was registered Assert.Single(_mocks); @@ -235,7 +233,6 @@ private IFileSystemWatcherWrapper WatcherFactoryWithMemory() private void AssertContainsWatcherFor(string path) { - var _ = _uut?.FileWatchers[path]; var foundMocks = ( from mock in _mocks where HasPropertySetTo(mock, watcher => watcher.Path = path) From d85bf1a95f6b388c9d74f08a7197bd85eb7e2fd7 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 5 Jan 2023 15:18:31 +0100 Subject: [PATCH 71/92] cosmetics --- Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs | 8 ++++++-- Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs | 3 +-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs index d24c610..47c6608 100644 --- a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs @@ -78,7 +78,7 @@ public void Init() RegisterFileWatcher(_watchPath); RegisterAdditionalFileWatchersForSymLinkDirs(_watchPath); } - + private void RegisterFileWatcher(string path) { _logger($"Registering file watcher for {path}"); @@ -169,6 +169,10 @@ private void ProcessRenamedEvent(RenamedEventArgs e) } + /// + /// Safely register a file watcher for a symbolic link directory. Used at startup as well as callback on file creation. + /// + /// internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) { try @@ -216,7 +220,7 @@ internal List GetFileWatchers() /// - /// Dispose the instance + /// Stop raising events and Dispose all filewatchers /// public void Dispose() { diff --git a/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs b/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs index 55ac909..d146e31 100644 --- a/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs +++ b/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs @@ -253,5 +253,4 @@ private static bool HasPropertySetTo(Mock mock, Actio return false; } } - -} \ No newline at end of file +} From fefa356229a7b306f9cec64c7e2ac4d4686398aa Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 5 Jan 2023 15:59:56 +0100 Subject: [PATCH 72/92] cosmetics --- .../Helpers/SymlinkAwareFileWatcher.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs index 47c6608..1e7efff 100644 --- a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs @@ -66,11 +66,11 @@ internal Func GetDirectoryInfosFunc public SymlinkAwareFileWatcher(string path, Action onEvent, Action onError, Func watcherFactory, Action logger) { - _logger = logger; - _watcherFactory = watcherFactory; _watchPath = path; _eventCallback = onEvent; _onError = onError; + _watcherFactory = watcherFactory; + _logger = logger; } public void Init() @@ -108,6 +108,12 @@ private void SetFileWatcherProperties(IFileSystemWatcherWrapper fileWatcher, str fileWatcher.InternalBufferSize = 32768; } + + private bool IsRootPath(string path) + { + return _watchPath == path; + } + private void RegisterFileWatcherEventHandlers(IFileSystemWatcherWrapper fileWatcher) { fileWatcher.Created += (_, e) => ProcessEvent(e, ChangeType.CREATED); @@ -121,12 +127,6 @@ private void RegisterFileWatcherEventHandlers(IFileSystemWatcherWrapper fileWatc fileWatcher.Deleted += UnregisterFileWatcherForSymbolicLinkDir; } - private bool IsRootPath(string path) - { - return _watchPath == path; - } - - /// /// Recursively find sym link dir and register them. /// Background: the native filewatcher does not follow symlinks so they need to be treated separately. From 41b4e4cc06cd73d260648aea190b7351dd6317d9 Mon Sep 17 00:00:00 2001 From: Maik Toepfer Date: Thu, 5 Jan 2023 17:51:31 +0100 Subject: [PATCH 73/92] tests run on Linux --- .../FileWatcherExIntegrationTest.cs | 102 ++++++++++-------- .../FileWatcherExTests.csproj | 2 +- 2 files changed, 57 insertions(+), 47 deletions(-) diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 3c9fc00..cdf5d8d 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -2,6 +2,7 @@ using FileWatcherEx; using FileWatcherExTests.Helper; using Xunit; +using Xunit.Abstractions; namespace FileWatcherExTests; @@ -14,15 +15,16 @@ public class FileWatcherExIntegrationTest : IDisposable private readonly ConcurrentQueue _events; private readonly FileSystemWatcherEx _fileWatcher; private readonly ReplayFileSystemWatcherFactory _replayFileSystemWatcherFactory; + private readonly TempDir _tempDir; - public FileWatcherExIntegrationTest() + public FileWatcherExIntegrationTest(ITestOutputHelper testOutputHelper) { // setup before each test run _events = new ConcurrentQueue(); _replayFileSystemWatcherFactory = new ReplayFileSystemWatcherFactory(); - const string recordingDir = @"C:\temp\fwtest"; - _fileWatcher = new FileSystemWatcherEx(recordingDir); + _tempDir = new TempDir(); + _fileWatcher = new FileSystemWatcherEx(_tempDir.FullPath, testOutputHelper.WriteLine); _fileWatcher.FileSystemWatcherFactory = () => _replayFileSystemWatcherFactory.Create(); _fileWatcher.IncludeSubdirectories = true; @@ -36,12 +38,12 @@ public FileWatcherExIntegrationTest() [Fact] public void Create_Single_File() { - StartFileWatcherAndReplay(@"scenario\create_file.csv"); + StartFileWatcherAndReplay(@"scenario/create_file.csv"); Assert.Single(_events); var ev = _events.First(); Assert.Equal(ChangeType.CREATED, ev.ChangeType); - Assert.Equal(@"C:\temp\fwtest\a.txt", ev.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\a.txt", ev.FullPath); Assert.Equal("", ev.OldFullPath); } @@ -49,18 +51,18 @@ public void Create_Single_File() [Fact] public void Create_And_Remove_Single_File() { - StartFileWatcherAndReplay(@"scenario\create_and_remove_file.csv"); + StartFileWatcherAndReplay(@"scenario/create_and_remove_file.csv"); Assert.Equal(2, _events.Count); var ev1 = _events.ToList()[0]; var ev2 = _events.ToList()[1]; Assert.Equal(ChangeType.CREATED, ev1.ChangeType); - Assert.Equal(@"C:\temp\fwtest\a.txt", ev1.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\a.txt", ev1.FullPath); Assert.Equal("", ev1.OldFullPath); Assert.Equal(ChangeType.DELETED, ev2.ChangeType); - Assert.Equal(@"C:\temp\fwtest\a.txt", ev2.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\a.txt", ev2.FullPath); Assert.Equal("", ev2.OldFullPath); } @@ -68,7 +70,7 @@ public void Create_And_Remove_Single_File() [Fact] public void Create_Rename_And_Remove_Single_File() { - StartFileWatcherAndReplay(@"scenario\create_rename_and_remove_file.csv"); + StartFileWatcherAndReplay(@"scenario/create_rename_and_remove_file.csv"); Assert.Equal(3, _events.Count); var ev1 = _events.ToList()[0]; @@ -76,14 +78,14 @@ public void Create_Rename_And_Remove_Single_File() var ev3 = _events.ToList()[2]; Assert.Equal(ChangeType.CREATED, ev1.ChangeType); - Assert.Equal(@"C:\temp\fwtest\a.txt", ev1.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\a.txt", ev1.FullPath); Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); - Assert.Equal(@"C:\temp\fwtest\b.txt", ev2.FullPath); - Assert.Equal(@"C:\temp\fwtest\a.txt", ev2.OldFullPath); + AssertEqualNormalized(@"C:\temp\fwtest\b.txt", ev2.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\a.txt", ev2.OldFullPath); Assert.Equal(ChangeType.DELETED, ev3.ChangeType); - Assert.Equal(@"C:\temp\fwtest\b.txt", ev3.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\b.txt", ev3.FullPath); Assert.Equal("", ev3.OldFullPath); } @@ -92,12 +94,12 @@ public void Create_Rename_And_Remove_Single_File() // filters out 2nd "changed" event public void Create_Single_File_Via_WSL2() { - StartFileWatcherAndReplay(@"scenario\create_file_wsl2.csv"); + StartFileWatcherAndReplay(@"scenario/create_file_wsl2.csv"); Assert.Single(_events); var ev = _events.First(); Assert.Equal(ChangeType.CREATED, ev.ChangeType); - Assert.Equal(@"C:\temp\fwtest\a.txt", ev.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\a.txt", ev.FullPath); Assert.Equal("", ev.OldFullPath); } @@ -107,12 +109,12 @@ public void Create_Single_File_Via_WSL2() // resulting event is just "created" with the filename taken from "renamed" public void Create_And_Rename_Single_File_Via_WSL2() { - StartFileWatcherAndReplay(@"scenario\create_and_rename_file_wsl2.csv"); + StartFileWatcherAndReplay(@"scenario/create_and_rename_file_wsl2.csv"); Assert.Single(_events); var ev = _events.First(); Assert.Equal(ChangeType.CREATED, ev.ChangeType); - Assert.Equal(@"C:\temp\fwtest\b.txt", ev.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\b.txt", ev.FullPath); Assert.Null(ev.OldFullPath); } @@ -120,7 +122,7 @@ public void Create_And_Rename_Single_File_Via_WSL2() [Fact] public void Create_Rename_And_Remove_Single_File_Via_WSL2() { - StartFileWatcherAndReplay(@"scenario\create_rename_and_remove_file_wsl2.csv"); + StartFileWatcherAndReplay(@"scenario/create_rename_and_remove_file_wsl2.csv"); Assert.Empty(_events); } @@ -128,7 +130,7 @@ public void Create_Rename_And_Remove_Single_File_Via_WSL2() [Fact] public void Create_Rename_And_Remove_Single_File_With_Wait_Time_Via_WSL2() { - StartFileWatcherAndReplay(@"scenario\create_rename_and_remove_file_with_wait_time_wsl2.csv"); + StartFileWatcherAndReplay(@"scenario/create_rename_and_remove_file_with_wait_time_wsl2.csv"); Assert.Equal(3, _events.Count); var ev1 = _events.ToList()[0]; @@ -136,14 +138,14 @@ public void Create_Rename_And_Remove_Single_File_With_Wait_Time_Via_WSL2() var ev3 = _events.ToList()[2]; Assert.Equal(ChangeType.CREATED, ev1.ChangeType); - Assert.Equal(@"C:\temp\fwtest\a.txt", ev1.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\a.txt", ev1.FullPath); Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); - Assert.Equal(@"C:\temp\fwtest\b.txt", ev2.FullPath); - Assert.Equal(@"C:\temp\fwtest\a.txt", ev2.OldFullPath); + AssertEqualNormalized(@"C:\temp\fwtest\b.txt", ev2.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\a.txt", ev2.OldFullPath); Assert.Equal(ChangeType.DELETED, ev3.ChangeType); - Assert.Equal(@"C:\temp\fwtest\b.txt", ev3.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\b.txt", ev3.FullPath); Assert.Equal("", ev3.OldFullPath); } @@ -151,7 +153,7 @@ public void Create_Rename_And_Remove_Single_File_With_Wait_Time_Via_WSL2() [Fact] public void Manually_Create_And_Rename_File_Via_Windows_Explorer() { - StartFileWatcherAndReplay(@"scenario\create_and_rename_file_via_explorer.csv"); + StartFileWatcherAndReplay(@"scenario/create_and_rename_file_via_explorer.csv"); Assert.Equal(2, _events.Count); @@ -159,17 +161,17 @@ public void Manually_Create_And_Rename_File_Via_Windows_Explorer() var ev2 = _events.ToList()[1]; Assert.Equal(ChangeType.CREATED, ev1.ChangeType); - Assert.Equal(@"C:\temp\fwtest\New Text Document.txt", ev1.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\New Text Document.txt", ev1.FullPath); Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); - Assert.Equal(@"C:\temp\fwtest\foo.txt", ev2.FullPath); - Assert.Equal(@"C:\temp\fwtest\New Text Document.txt", ev2.OldFullPath); + AssertEqualNormalized(@"C:\temp\fwtest\foo.txt", ev2.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\New Text Document.txt", ev2.OldFullPath); } [Fact] public void Manually_Create_Rename_And_Delete_File_Via_Windows_Explorer() { - StartFileWatcherAndReplay(@"scenario\create_rename_and_delete_file_via_explorer.csv"); + StartFileWatcherAndReplay(@"scenario/create_rename_and_delete_file_via_explorer.csv"); Assert.Equal(3, _events.Count); var ev1 = _events.ToList()[0]; @@ -177,56 +179,56 @@ public void Manually_Create_Rename_And_Delete_File_Via_Windows_Explorer() var ev3 = _events.ToList()[2]; Assert.Equal(ChangeType.CREATED, ev1.ChangeType); - Assert.Equal(@"C:\temp\fwtest\New Text Document.txt", ev1.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\New Text Document.txt", ev1.FullPath); Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); - Assert.Equal(@"C:\temp\fwtest\foo.txt", ev2.FullPath); - Assert.Equal(@"C:\temp\fwtest\New Text Document.txt", ev2.OldFullPath); + AssertEqualNormalized(@"C:\temp\fwtest\foo.txt", ev2.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\New Text Document.txt", ev2.OldFullPath); Assert.Equal(ChangeType.DELETED, ev3.ChangeType); - Assert.Equal(@"C:\temp\fwtest\foo.txt", ev3.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\foo.txt", ev3.FullPath); Assert.Equal("", ev3.OldFullPath); } [Fact] public void Download_Image_Via_Edge_Browser() { - StartFileWatcherAndReplay(@"scenario\download_image_via_Edge_browser.csv"); + StartFileWatcherAndReplay(@"scenario/download_image_via_Edge_browser.csv"); Assert.Equal(2, _events.Count); var ev1 = _events.ToList()[0]; var ev2 = _events.ToList()[1]; Assert.Equal(ChangeType.CREATED, ev1.ChangeType); - Assert.Equal(@"C:\temp\fwtest\test.png.crdownload", ev1.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\test.png.crdownload", ev1.FullPath); Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); - Assert.Equal(@"C:\temp\fwtest\test.png", ev2.FullPath); - Assert.Equal(@"C:\temp\fwtest\test.png.crdownload", ev2.OldFullPath); + AssertEqualNormalized(@"C:\temp\fwtest\test.png", ev2.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\test.png.crdownload", ev2.OldFullPath); } // instantly removed file is not in the events list [Fact] public void Create_Sub_Directory_Add_And_Remove_File() { - StartFileWatcherAndReplay(@"scenario\create_subdirectory_add_and_remove_file.csv"); + StartFileWatcherAndReplay(@"scenario/create_subdirectory_add_and_remove_file.csv"); Assert.Equal(2, _events.Count); var ev1 = _events.ToList()[0]; var ev2 = _events.ToList()[1]; Assert.Equal(ChangeType.CREATED, ev1.ChangeType); - Assert.Equal(@"C:\temp\fwtest\subdir", ev1.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\subdir", ev1.FullPath); Assert.Equal(ChangeType.CHANGED, ev2.ChangeType); - Assert.Equal(@"C:\temp\fwtest\subdir", ev2.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\subdir", ev2.FullPath); Assert.Equal(@"", ev2.OldFullPath); } - + [Fact] public void Create_Sub_Directory_Add_And_Remove_File_With_Sleep() { - StartFileWatcherAndReplay(@"scenario\create_subdirectory_add_and_remove_file_with_sleep.csv"); + StartFileWatcherAndReplay(@"scenario/create_subdirectory_add_and_remove_file_with_sleep.csv"); Assert.Equal(4, _events.Count); var ev1 = _events.ToList()[0]; @@ -235,19 +237,19 @@ public void Create_Sub_Directory_Add_And_Remove_File_With_Sleep() var ev4 = _events.ToList()[3]; Assert.Equal(ChangeType.CREATED, ev1.ChangeType); - Assert.Equal(@"C:\temp\fwtest\subdir", ev1.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\subdir", ev1.FullPath); Assert.Equal(ChangeType.CREATED, ev2.ChangeType); - Assert.Equal(@"C:\temp\fwtest\subdir\a.txt", ev2.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\subdir\a.txt", ev2.FullPath); Assert.Equal(@"", ev2.OldFullPath); // TODO this could be filtered out Assert.Equal(ChangeType.CHANGED, ev3.ChangeType); - Assert.Equal(@"C:\temp\fwtest\subdir", ev3.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\subdir", ev3.FullPath); Assert.Equal(@"", ev3.OldFullPath); Assert.Equal(ChangeType.DELETED, ev4.ChangeType); - Assert.Equal(@"C:\temp\fwtest\subdir\a.txt", ev4.FullPath); + AssertEqualNormalized(@"C:\temp\fwtest\subdir\a.txt", ev4.FullPath); Assert.Equal(@"", ev4.OldFullPath); } @@ -330,6 +332,7 @@ public void Simple_Real_File_System_Test() public void Dispose() { _fileWatcher.Dispose(); + _tempDir.Dispose(); } private void StartFileWatcherAndReplay(string csvFile) @@ -357,4 +360,11 @@ public ReplayFileSystemWatcherWrapper Create() // This is the one which is registered first and watches the root directory. public ReplayFileSystemWatcherWrapper RootWatcher => _wrappers[0]; } -} \ No newline at end of file + + // little hack to make the path comparision platform independent + private static void AssertEqualNormalized(string expected, string? actual) + { + actual = actual?.Replace("/", @"\"); + Assert.Equal(expected, actual); + } +} diff --git a/Source/FileWatcherExTests/FileWatcherExTests.csproj b/Source/FileWatcherExTests/FileWatcherExTests.csproj index 610739e..132f3a6 100644 --- a/Source/FileWatcherExTests/FileWatcherExTests.csproj +++ b/Source/FileWatcherExTests/FileWatcherExTests.csproj @@ -1,7 +1,7 @@ - net6.0 + net7.0 enable enable false From 27cc43777ad6d2e5f54e905c911f93f415cf7f19 Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Mon, 23 Jan 2023 14:17:59 +0800 Subject: [PATCH 74/92] change default path in Demo form --- Source/Demo/Form1.Designer.cs | 2 +- Source/Demo/Form1.cs | 6 +++--- Source/FileWatcherEx/FileSystemWatcherEx.cs | 4 +++- .../Helpers/SymlinkAwareFileWatcher.cs | 15 ++++++++------- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Source/Demo/Form1.Designer.cs b/Source/Demo/Form1.Designer.cs index 797ed49..5046d93 100644 --- a/Source/Demo/Form1.Designer.cs +++ b/Source/Demo/Form1.Designer.cs @@ -74,7 +74,7 @@ private void InitializeComponent() this.txtPath.Name = "txtPath"; this.txtPath.Size = new System.Drawing.Size(508, 57); this.txtPath.TabIndex = 2; - this.txtPath.Text = "C:\\Users\\d2pha\\Desktop\\New folder"; + this.txtPath.Text = "C:\\"; // // btnStart // diff --git a/Source/Demo/Form1.cs b/Source/Demo/Form1.cs index c5a739c..25b17d2 100644 --- a/Source/Demo/Form1.cs +++ b/Source/Demo/Form1.cs @@ -26,7 +26,7 @@ private void BtnStart_Click(object sender, EventArgs e) _fw.SynchronizingObject = this; _fw.IncludeSubdirectories = true; - + _fw.Start(); btnStart.Enabled = false; @@ -78,9 +78,9 @@ private void FW_OnRenamed(object? sender, FileChangedEvent e) private void FW_OnLog(string value) { - txtConsole.Text += $@"[log] {value}" + "\r\n";; + txtConsole.Text += $@"[log] {value}" + "\r\n"; } - + private void BtnStop_Click(object sender, EventArgs e) { _fw.Stop(); diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index d052f79..06e6d2f 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -86,6 +86,7 @@ public string Filter #endregion + #region Public Events /// @@ -141,6 +142,7 @@ public FileSystemWatcherEx(string folderPath = "", Action? logger = null _logger = logger ?? (_ => {}) ; } + /// /// Start watching files /// @@ -273,6 +275,7 @@ void OnError(ErrorEventArgs e) _watcher.Init(); } + internal void StartForTesting( Func getFileAttributesFunc, Func getDirectoryInfosFunc) @@ -308,7 +311,6 @@ public void Dispose() } - private void Thread_DoingWork(CancellationToken cancelToken) { while (true) diff --git a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs index 1e7efff..f97de2a 100644 --- a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs @@ -48,15 +48,16 @@ internal Func GetDirectoryInfosFunc public Collection Filters { get; } = new(); public ISynchronizeInvoke? SynchronizingObject { get; set; } - - + + /// - /// Create new instance of FileSystemWatcherWrapper - /// + /// Create new instance of . /// Object creation follows this order: - /// 1) create new instance - /// 2) set properties (optional) - /// 3) call init() (mandatory) + /// + /// 1) create new instance + /// 2) set properties (optional) + /// 3) call init() (mandatory) + /// /// /// Full folder path to watcher /// onEvent callback From 41c9564ce19d51cb22dcdf786dcac569c35048cb Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Mon, 23 Jan 2023 14:29:29 +0800 Subject: [PATCH 75/92] Update FileWatcherEx.csproj --- Source/FileWatcherEx/FileWatcherEx.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/FileWatcherEx/FileWatcherEx.csproj b/Source/FileWatcherEx/FileWatcherEx.csproj index e755cd5..7979b1e 100644 --- a/Source/FileWatcherEx/FileWatcherEx.csproj +++ b/Source/FileWatcherEx/FileWatcherEx.csproj @@ -13,7 +13,7 @@ A wrapper of FileSystemWatcher to standardize the events and avoid false change notifications, used in ImageGlass project (https://imageglass.org). This project is based on the VSCode FileWatcher: https://github.com/Microsoft/vscode-filewatcher-windows. True $(Version) - 2.2.0 + 2.3.0 MIT See https://github.com/d2phap/FileWatcherEx/releases d2phap From 0e7ca5b36417ea20d9d154dfcc221945bfdf252f Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Thu, 30 Mar 2023 15:34:18 +0800 Subject: [PATCH 76/92] fixed: calling Stop() throws ObjectDisposed exception --- Source/Demo/Form1.cs | 3 ++- .../FileSystemEventRecorder/FileSystemEventRecorder.cs | 1 - Source/FileWatcherEx/FileSystemWatcherEx.cs | 8 +++++--- Source/FileWatcherEx/FileWatcherEx.csproj | 9 +++++++-- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Source/Demo/Form1.cs b/Source/Demo/Form1.cs index 25b17d2..5741081 100644 --- a/Source/Demo/Form1.cs +++ b/Source/Demo/Form1.cs @@ -29,7 +29,7 @@ private void BtnStart_Click(object sender, EventArgs e) _fw.Start(); - btnStart.Enabled = false; + btnStart.Enabled = true; btnSelectFolder.Enabled = false; txtPath.Enabled = false; btnStop.Enabled = true; @@ -89,6 +89,7 @@ private void BtnStop_Click(object sender, EventArgs e) btnSelectFolder.Enabled = true; txtPath.Enabled = true; btnStop.Enabled = false; + btnStop.Enabled = true; } diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs index ffe7df0..cfdba6d 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs @@ -2,7 +2,6 @@ using System.Diagnostics; using System.Globalization; using CsvHelper; -using FileWatcherEx; namespace FileSystemEventRecorder; diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 06e6d2f..5a3a58a 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -149,6 +149,7 @@ public FileSystemWatcherEx(string folderPath = "", Action? logger = null public void Start() { if (!Directory.Exists(FolderPath)) return; + Stop(); _processor = new EventProcessor((e) => @@ -293,10 +294,11 @@ internal void StartForTesting( public void Stop() { _watcher?.Dispose(); + _watcher = null; // stop the thread - _cancelSource?.Cancel(); _cancelSource?.Dispose(); + _cancelSource = null; } @@ -305,8 +307,8 @@ public void Stop() /// public void Dispose() { - _watcher?.Dispose(); - _cancelSource?.Dispose(); + Stop(); + GC.SuppressFinalize(this); } diff --git a/Source/FileWatcherEx/FileWatcherEx.csproj b/Source/FileWatcherEx/FileWatcherEx.csproj index 7979b1e..e8e856a 100644 --- a/Source/FileWatcherEx/FileWatcherEx.csproj +++ b/Source/FileWatcherEx/FileWatcherEx.csproj @@ -13,16 +13,21 @@ A wrapper of FileSystemWatcher to standardize the events and avoid false change notifications, used in ImageGlass project (https://imageglass.org). This project is based on the VSCode FileWatcher: https://github.com/Microsoft/vscode-filewatcher-windows. True $(Version) - 2.3.0 - MIT + 2.4.0 See https://github.com/d2phap/FileWatcherEx/releases d2phap FileWatcherEx - A file system watcher True snupkg + LICENSE + False + + True + \ + True \ From c92a71d75252491999347f79875e37cfb9cf9815 Mon Sep 17 00:00:00 2001 From: Chris Yeninas <844685+PhantomGamers@users.noreply.github.com> Date: Fri, 26 May 2023 23:26:47 -0400 Subject: [PATCH 77/92] fix IncludeSubdirectories check IncludeSubdirectories was being checked after searching for all subdirectories, for each subdirectory found. Let's check before checking for subdirectories instead --- .../Helpers/SymlinkAwareFileWatcher.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs index f97de2a..81efe53 100644 --- a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs @@ -136,12 +136,14 @@ private void RegisterAdditionalFileWatchersForSymLinkDirs(string path) { TryRegisterFileWatcherForSymbolicLinkDir(path); - if (Directory.Exists(path)) + if (!IncludeSubdirectories || !Directory.Exists(path)) { - foreach (var dirInfo in GetDirectoryInfosFunc(path)) - { - RegisterAdditionalFileWatchersForSymLinkDirs(dirInfo.FullName); - } + return; + } + + foreach (var dirInfo in GetDirectoryInfosFunc(path)) + { + RegisterAdditionalFileWatchersForSymLinkDirs(dirInfo.FullName); } } @@ -178,7 +180,7 @@ internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) { try { - if (IsSymbolicLinkDirectory(path) && IncludeSubdirectories && !FileWatchers.ContainsKey(path)) + if (IsSymbolicLinkDirectory(path) && !FileWatchers.ContainsKey(path)) { _logger($"Directory {path} is a symbolic link dir. Will register additional file watcher."); RegisterFileWatcher(path); @@ -231,4 +233,4 @@ public void Dispose() watcher.Dispose(); } } -} \ No newline at end of file +} From 23d067ceae5bc4b9b3be8b00a2d1195178efb4fe Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Sat, 27 May 2023 11:56:52 +0800 Subject: [PATCH 78/92] Create dotnet.yml --- .github/workflows/dotnet.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/dotnet.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..25f43e7 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,28 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 6.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal From 2e2359be4b97a72b31c5c38eca3c32a8093b67bd Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Sat, 27 May 2023 11:59:50 +0800 Subject: [PATCH 79/92] Update dotnet.yml --- .github/workflows/dotnet.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 25f43e7..fe93e0c 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -20,6 +20,8 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: 6.0.x + - name: Go to Source folder + run: cd Source - name: Restore dependencies run: dotnet restore - name: Build From 0f0a1c9b619646f1e5153c488f439a536bc2138c Mon Sep 17 00:00:00 2001 From: Chris Yeninas <844685+PhantomGamers@users.noreply.github.com> Date: Sat, 27 May 2023 00:02:33 -0400 Subject: [PATCH 80/92] fix tests --- Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs index 81efe53..8811ce9 100644 --- a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs @@ -180,7 +180,7 @@ internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) { try { - if (IsSymbolicLinkDirectory(path) && !FileWatchers.ContainsKey(path)) + if (IsSymbolicLinkDirectory(path) && IncludeSubdirectories && Include!FileWatchers.ContainsKey(path)) { _logger($"Directory {path} is a symbolic link dir. Will register additional file watcher."); RegisterFileWatcher(path); From 23be561342442785f7456b01c94fd59d8a1f350b Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Sat, 27 May 2023 12:03:06 +0800 Subject: [PATCH 81/92] Update dotnet.yml --- .github/workflows/dotnet.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index fe93e0c..b34562f 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -9,6 +9,10 @@ on: pull_request: branches: [ "main" ] +defaults: + run: + working-directory: Source + jobs: build: @@ -20,8 +24,6 @@ jobs: uses: actions/setup-dotnet@v3 with: dotnet-version: 6.0.x - - name: Go to Source folder - run: cd Source - name: Restore dependencies run: dotnet restore - name: Build From 1c01c4febf45e3c6afe890526888cca9c6dca695 Mon Sep 17 00:00:00 2001 From: Chris Yeninas <844685+PhantomGamers@users.noreply.github.com> Date: Sat, 27 May 2023 00:04:45 -0400 Subject: [PATCH 82/92] Update SymlinkAwareFileWatcher.cs --- Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs index 8811ce9..f943541 100644 --- a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs @@ -180,7 +180,7 @@ internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) { try { - if (IsSymbolicLinkDirectory(path) && IncludeSubdirectories && Include!FileWatchers.ContainsKey(path)) + if (IsSymbolicLinkDirectory(path) && IncludeSubdirectories && !FileWatchers.ContainsKey(path)) { _logger($"Directory {path} is a symbolic link dir. Will register additional file watcher."); RegisterFileWatcher(path); From 5851a02a5f3f14271a0dfd38d67d78e6a189085d Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Sat, 27 May 2023 12:15:28 +0800 Subject: [PATCH 83/92] Update dotnet.yml --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index b34562f..abb64a5 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -16,7 +16,7 @@ defaults: jobs: build: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - uses: actions/checkout@v3 From e335305803b123048d6629870b52fa9656108930 Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Sat, 27 May 2023 12:24:26 +0800 Subject: [PATCH 84/92] Demo: show error msgbox --- Source/Demo/Form1.cs | 17 ++++++++++++----- Source/FileWatcherEx/FileSystemWatcherEx.cs | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Source/Demo/Form1.cs b/Source/Demo/Form1.cs index 5741081..92a035a 100644 --- a/Source/Demo/Form1.cs +++ b/Source/Demo/Form1.cs @@ -27,12 +27,19 @@ private void BtnStart_Click(object sender, EventArgs e) _fw.SynchronizingObject = this; _fw.IncludeSubdirectories = true; - _fw.Start(); + try + { + _fw.Start(); - btnStart.Enabled = true; - btnSelectFolder.Enabled = false; - txtPath.Enabled = false; - btnStop.Enabled = true; + btnStart.Enabled = true; + btnSelectFolder.Enabled = false; + txtPath.Enabled = false; + btnStop.Enabled = true; + } + catch (Exception ex) + { + MessageBox.Show(ex.Message); + } } private void FW_OnError(object? sender, ErrorEventArgs e) diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 5a3a58a..2fc4204 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -272,7 +272,7 @@ void OnError(ErrorEventArgs e) SynchronizingObject = SynchronizingObject, EnableRaisingEvents = true }; - Filters.ToList().ForEach(filter => _watcher.Filters.Add(filter)); + Filters.ToList().ForEach(_watcher.Filters.Add); _watcher.Init(); } From 79998b7148a02d21a599f8f74e30c19143dce9ea Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Sat, 27 May 2023 12:24:31 +0800 Subject: [PATCH 85/92] bump version --- Source/FileWatcherEx/FileWatcherEx.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/FileWatcherEx/FileWatcherEx.csproj b/Source/FileWatcherEx/FileWatcherEx.csproj index e8e856a..a9b413d 100644 --- a/Source/FileWatcherEx/FileWatcherEx.csproj +++ b/Source/FileWatcherEx/FileWatcherEx.csproj @@ -13,7 +13,7 @@ A wrapper of FileSystemWatcher to standardize the events and avoid false change notifications, used in ImageGlass project (https://imageglass.org). This project is based on the VSCode FileWatcher: https://github.com/Microsoft/vscode-filewatcher-windows. True $(Version) - 2.4.0 + 2.5.0 See https://github.com/d2phap/FileWatcherEx/releases d2phap FileWatcherEx - A file system watcher From 53c63184e366982a95cd6ae5719327c22a4a9e8c Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Thu, 16 Nov 2023 22:54:48 +0800 Subject: [PATCH 86/92] .net 8 + update deps --- Source/Demo/Demo.WinForms.csproj | 2 +- .../FileSystemEventRecorder.csproj | 2 +- Source/FileWatcherEx/FileWatcherEx.csproj | 4 ++-- Source/FileWatcherExTests/FileWatcherExTests.csproj | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Source/Demo/Demo.WinForms.csproj b/Source/Demo/Demo.WinForms.csproj index 592a245..b1c23ba 100644 --- a/Source/Demo/Demo.WinForms.csproj +++ b/Source/Demo/Demo.WinForms.csproj @@ -2,7 +2,7 @@ WinExe - net6.0-windows + net8.0-windows enable true enable diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj b/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj index 8ccbdba..3c28d70 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 enable enable diff --git a/Source/FileWatcherEx/FileWatcherEx.csproj b/Source/FileWatcherEx/FileWatcherEx.csproj index a9b413d..cc3fc0f 100644 --- a/Source/FileWatcherEx/FileWatcherEx.csproj +++ b/Source/FileWatcherEx/FileWatcherEx.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0 + net6.0;net7.0;net8.0 enable enable Copyright © 2018-2023 Duong Dieu Phap @@ -13,7 +13,7 @@ A wrapper of FileSystemWatcher to standardize the events and avoid false change notifications, used in ImageGlass project (https://imageglass.org). This project is based on the VSCode FileWatcher: https://github.com/Microsoft/vscode-filewatcher-windows. True $(Version) - 2.5.0 + 2.6.0 See https://github.com/d2phap/FileWatcherEx/releases d2phap FileWatcherEx - A file system watcher diff --git a/Source/FileWatcherExTests/FileWatcherExTests.csproj b/Source/FileWatcherExTests/FileWatcherExTests.csproj index 132f3a6..7cd3626 100644 --- a/Source/FileWatcherExTests/FileWatcherExTests.csproj +++ b/Source/FileWatcherExTests/FileWatcherExTests.csproj @@ -1,7 +1,7 @@ - net7.0 + net8.0 enable enable false @@ -9,14 +9,14 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From e56587b7d27322a2a4ece9f3ca640d860bb7fd74 Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Thu, 16 Nov 2023 22:59:21 +0800 Subject: [PATCH 87/92] Update dotnet.yml .net 8 --- .github/workflows/dotnet.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index abb64a5..783009a 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -23,7 +23,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - name: Build From 781f19d471c03bbf5de922ecc9e140a39cbab76a Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Thu, 2 May 2024 16:52:53 +0800 Subject: [PATCH 88/92] Update FUNDING.yml --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 1eea57e..c014174 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -5,4 +5,4 @@ patreon: d2phap open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel -custom: ["https://donorbox.org/imageglass", "https://www.paypal.me/d2phap"] +custom: ["https://donate.stripe.com/6oE15Kab3740du828a", "https://www.paypal.me/d2phap"] From 57bbc77f3376678186d5e89a464f54628759d010 Mon Sep 17 00:00:00 2001 From: Phap Dieu Duong Date: Sat, 2 Aug 2025 21:26:16 +0700 Subject: [PATCH 89/92] updated dependencies and code style --- LICENSE | 4 +- README.md | 5 +- Source/Demo/Demo.WinForms.csproj | 2 +- .../FileSystemEventRecorder.cs | 70 ++++++++-------- .../FileSystemEventRecorder.csproj | 4 +- Source/FileWatcherEx/FileSystemWatcherEx.cs | 46 ++++------- Source/FileWatcherEx/FileWatcherEx.csproj | 7 +- .../FileWatcherEx/Helpers/EventNormalizer.cs | 8 +- .../FileWatcherEx/Helpers/EventProcessor.cs | 23 +++--- .../Helpers/FileSystemWatcherWrapper.cs | 6 +- .../Helpers/SymlinkAwareFileWatcher.cs | 81 +++++++++---------- .../FileWatcherExTests.csproj | 12 +-- .../ReplayFileSystemWatcherWrapper.cs | 54 ++++++------- 13 files changed, 151 insertions(+), 171 deletions(-) diff --git a/LICENSE b/LICENSE index e693ab6..15f27b7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +MIT License -Copyright (c) 2022 Duong Dieu Phap +Copyright (c) 2025 Duong Dieu Phap Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 67c3169..a809262 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FileWatcherEx for Windows +# FileWatcherEx for Windows A wrapper of `System.IO.FileSystemWatcher` to standardize the events and avoid false change notifications. It has been being used in [ImageGlass - A lightweight, versatile image viewer](https://github.com/d2phap/ImageGlass) project. This project is based on the *VSCode FileWatcher*: https://github.com/Microsoft/vscode-filewatcher-windows @@ -14,7 +14,7 @@ This project is based on the *VSCode FileWatcher*: https://github.com/Microsoft/ ## Features - Standardizes the events of `System.IO.FileSystemWatcher`. - No false change notifications when a file system item is created, deleted, changed or renamed. -- Supports .NET 6.0, 7.0. +- Supports .NET 6.0, 7.0, 8.0, 9.0 ## Installation Run the command: @@ -63,6 +63,5 @@ void FW_OnRenamed(object sender, FileChangedEvent e) - [GitHub sponsor](https://github.com/sponsors/d2phap) - [Patreon](https://www.patreon.com/d2phap) - [PayPal](https://www.paypal.me/d2phap) -- [Wire Transfers](https://donorbox.org/imageglass) Thanks for your gratitude and finance help! diff --git a/Source/Demo/Demo.WinForms.csproj b/Source/Demo/Demo.WinForms.csproj index b1c23ba..89ebc04 100644 --- a/Source/Demo/Demo.WinForms.csproj +++ b/Source/Demo/Demo.WinForms.csproj @@ -2,7 +2,7 @@ WinExe - net8.0-windows + net9.0-windows enable true enable diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs index cfdba6d..45a43f8 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs @@ -1,14 +1,14 @@ -using System.Collections.Concurrent; +using CsvHelper; +using System.Collections.Concurrent; using System.Diagnostics; using System.Globalization; -using CsvHelper; namespace FileSystemEventRecorder; // event received from C# FileSystemWatcher internal record EventRecord( - string FullPath, - string EventName, + string FullPath, + string EventName, string? OldFullPath, // only provided by "rename" event long NowInTicks ); @@ -33,7 +33,7 @@ public static class FileSystemEventRecords public static void Main(string[] args) { - var (watchedDirectory, csvOutputFile) = ProcessArguments(args); + var (watchedDirectory, csvOutputFile) = ProcessArguments(args); var watcher = new FileSystemWatcher(); watcher.Path = watchedDirectory; @@ -48,7 +48,7 @@ public static void Main(string[] args) EventRecords.Enqueue(new EventRecord(ev.FullPath, "deleted", null, Stopwatch.GetTimestamp())); watcher.Changed += (_, ev) => - EventRecords.Enqueue(new EventRecord(ev.FullPath, "changed", null, Stopwatch.GetTimestamp())); + EventRecords.Enqueue(new EventRecord(ev.FullPath, "changed", null, Stopwatch.GetTimestamp())); watcher.Renamed += (_, ev) => EventRecords.Enqueue(new EventRecord(ev.FullPath, "renamed", ev.OldFullPath, Stopwatch.GetTimestamp())); watcher.Error += (_, ev) => @@ -96,7 +96,7 @@ private static void ProcessQueueAndWriteToDisk(string csvOutputFile) else { Console.WriteLine($"Recorded {EventRecords.Count} file system events."); - var records = MapToDiffTicks(); + var records = MapToDiffTicks; Console.WriteLine($"Writing CSV to {csvOutputFile}."); using (var writer = new StreamWriter(csvOutputFile)) @@ -110,35 +110,39 @@ private static void ProcessQueueAndWriteToDisk(string csvOutputFile) } // post-process queue. Calculate difference between previous and current event - private static IEnumerable MapToDiffTicks() + private static IEnumerable MapToDiffTicks { - List eventsWithDiffs = new(); - long previousTicks = 0; - foreach (var eventRecord in EventRecords) + get { - var diff = previousTicks switch + List eventsWithDiffs = []; + long previousTicks = 0; + + foreach (var eventRecord in EventRecords) { - 0 => 0, // first run - _ => eventRecord.NowInTicks - previousTicks - }; - - previousTicks = eventRecord.NowInTicks; - double diffInMilliseconds = Convert.ToInt64(new TimeSpan(diff).TotalMilliseconds); - - var directory = Path.GetDirectoryName(eventRecord.FullPath) ?? ""; - var fileName = Path.GetFileName(eventRecord.FullPath); - var oldFileName = Path.GetFileName(eventRecord.OldFullPath); - - var record = new EventRecordWithDiff( - directory, - fileName, - eventRecord.EventName, - oldFileName, - diff, - diffInMilliseconds); - eventsWithDiffs.Add(record); - } + var diff = previousTicks switch + { + 0 => 0, // first run + _ => eventRecord.NowInTicks - previousTicks + }; + + previousTicks = eventRecord.NowInTicks; + double diffInMilliseconds = Convert.ToInt64(new TimeSpan(diff).TotalMilliseconds); + + var directory = Path.GetDirectoryName(eventRecord.FullPath) ?? ""; + var fileName = Path.GetFileName(eventRecord.FullPath); + var oldFileName = Path.GetFileName(eventRecord.OldFullPath); + + var record = new EventRecordWithDiff( + directory, + fileName, + eventRecord.EventName, + oldFileName, + diff, + diffInMilliseconds); + eventsWithDiffs.Add(record); + } - return eventsWithDiffs; + return eventsWithDiffs; + } } } \ No newline at end of file diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj b/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj index 3c28d70..936cf26 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable @@ -12,7 +12,7 @@ - + diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index 2fc4204..aa14292 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -1,25 +1,28 @@  +using FileWatcherEx.Helpers; using System.Collections.Concurrent; using System.ComponentModel; -using FileWatcherEx.Helpers; namespace FileWatcherEx; + /// /// A wrapper of to standardize the events /// and avoid false change notifications. /// -public class FileSystemWatcherEx : IDisposable, IFileSystemWatcherEx +/// +/// Optional Action to log out library internals +public class FileSystemWatcherEx(string folderPath = "", Action? logger = null) : IDisposable, IFileSystemWatcherEx { #region Private Properties private Thread? _thread; private EventProcessor? _processor; - private readonly BlockingCollection _fileEventQueue = new(); + private readonly BlockingCollection _fileEventQueue = []; private SymlinkAwareFileWatcher? _watcher; private Func? _fswFactory; - private readonly Action _logger; + private readonly Action _logger = logger ?? (_ => { }); // Define the cancellation token. private CancellationTokenSource? _cancelSource; @@ -40,13 +43,13 @@ internal Func FileSystemWatcherFactory /// /// Gets or sets the path of the directory to watch. /// - public string FolderPath { get; set; } = ""; + public string FolderPath { get; set; } = folderPath; /// /// Gets the collection of all the filters used to determine what files are monitored in a directory. /// - public System.Collections.ObjectModel.Collection Filters { get; } = new(); + public System.Collections.ObjectModel.Collection Filters { get; } = []; /// @@ -131,17 +134,6 @@ public string Filter #endregion - /// - /// Initialize new instance of - /// - /// - /// Optional Action to log out library internals - public FileSystemWatcherEx(string folderPath = "", Action? logger = null) - { - FolderPath = folderPath; - _logger = logger ?? (_ => {}) ; - } - /// /// Start watching files @@ -164,15 +156,13 @@ void InvokeChangedEvent(object? sender, FileChangedEvent fileEvent) { if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) { - SynchronizingObject.Invoke(new Action(InvokeChangedEvent), new object[] { SynchronizingObject, e }); + SynchronizingObject.Invoke(new Action(InvokeChangedEvent), [SynchronizingObject, e]); } else { OnChanged?.Invoke(SynchronizingObject, e); } } - - break; case ChangeType.CREATED: @@ -183,15 +173,13 @@ void InvokeCreatedEvent(object? sender, FileChangedEvent fileEvent) { if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) { - SynchronizingObject.Invoke(new Action(InvokeCreatedEvent), new object[] { SynchronizingObject, e }); + SynchronizingObject.Invoke(new Action(InvokeCreatedEvent), args: [SynchronizingObject, e]); } else { OnCreated?.Invoke(SynchronizingObject, e); } } - - break; case ChangeType.DELETED: @@ -202,15 +190,13 @@ void InvokeDeletedEvent(object? sender, FileChangedEvent fileEvent) { if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) { - SynchronizingObject.Invoke(new Action(InvokeDeletedEvent), new object[] { SynchronizingObject, e }); + SynchronizingObject.Invoke(new Action(InvokeDeletedEvent), [SynchronizingObject, e]); } else { OnDeleted?.Invoke(SynchronizingObject, e); } } - - break; case ChangeType.RENAMED: @@ -221,15 +207,13 @@ void InvokeRenamedEvent(object? sender, FileChangedEvent fileEvent) { if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) { - SynchronizingObject.Invoke(new Action(InvokeRenamedEvent), new object[] { SynchronizingObject, e }); + SynchronizingObject.Invoke(new Action(InvokeRenamedEvent), [SynchronizingObject, e]); } else { OnRenamed?.Invoke(SynchronizingObject, e); } } - - break; default: @@ -237,7 +221,7 @@ void InvokeRenamedEvent(object? sender, FileChangedEvent fileEvent) } }, (log) => { - Console.WriteLine($"{Enum.GetName(typeof(ChangeType), ChangeType.LOG)} | {log}"); + Console.WriteLine($"{Enum.GetName(ChangeType.LOG)} | {log}"); }); _cancelSource = new CancellationTokenSource(); @@ -278,7 +262,7 @@ void OnError(ErrorEventArgs e) internal void StartForTesting( - Func getFileAttributesFunc, + Func getFileAttributesFunc, Func getDirectoryInfosFunc) { Start(); diff --git a/Source/FileWatcherEx/FileWatcherEx.csproj b/Source/FileWatcherEx/FileWatcherEx.csproj index cc3fc0f..afa2b55 100644 --- a/Source/FileWatcherEx/FileWatcherEx.csproj +++ b/Source/FileWatcherEx/FileWatcherEx.csproj @@ -1,10 +1,10 @@ - net6.0;net7.0;net8.0 + net6.0;net7.0;net8.0;net9.0 enable enable - Copyright © 2018-2023 Duong Dieu Phap + Copyright © 2018-2025 Duong Dieu Phap https://github.com/d2phap/FileWatcherEx README.md https://github.com/d2phap/FileWatcherEx @@ -13,7 +13,7 @@ A wrapper of FileSystemWatcher to standardize the events and avoid false change notifications, used in ImageGlass project (https://imageglass.org). This project is based on the VSCode FileWatcher: https://github.com/Microsoft/vscode-filewatcher-windows. True $(Version) - 2.6.0 + 2.7.0 See https://github.com/d2phap/FileWatcherEx/releases d2phap FileWatcherEx - A file system watcher @@ -21,6 +21,7 @@ snupkg LICENSE False + latest diff --git a/Source/FileWatcherEx/Helpers/EventNormalizer.cs b/Source/FileWatcherEx/Helpers/EventNormalizer.cs index 85f0578..bb7a37a 100644 --- a/Source/FileWatcherEx/Helpers/EventNormalizer.cs +++ b/Source/FileWatcherEx/Helpers/EventNormalizer.cs @@ -128,15 +128,15 @@ internal static bool IsParent(FileChangedEvent e, List deletedPaths) internal static bool IsParent(string path, string candidatePath) { // if exists, remove trailing "\" for both paths - candidatePath = candidatePath.TrimEnd('\\'); + candidatePath = candidatePath.TrimEnd('\\'); path = path.TrimEnd('\\'); - return path.IndexOf(candidatePath + '\\', StringComparison.Ordinal) == 0; + return path.StartsWith(candidatePath + '\\', StringComparison.Ordinal); } private class FileEventRepository { - private readonly Dictionary _mapPathToEvents = new(); + private readonly Dictionary _mapPathToEvents = []; public void AddOrUpdate(FileChangedEvent newEvent) { @@ -166,7 +166,7 @@ public void Remove(FileChangedEvent ev) public List Events() { - return _mapPathToEvents.Values.ToList(); + return [.. _mapPathToEvents.Values]; } } } diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index cda59f2..3232066 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -4,7 +4,7 @@ namespace FileWatcherEx.Helpers; -internal class EventProcessor +internal class EventProcessor(Action onEvent, Action onLogging) { /// /// Aggregate and only emit events when changes have stopped for this duration (in ms) @@ -16,25 +16,22 @@ internal class EventProcessor /// private readonly TimeSpan _eventSpamWarningThreshold = TimeSpan.FromMinutes(1); +#if NET9_0_OR_GREATER + private readonly Lock _lock = new(); +#else private readonly object _lock = new(); +#endif private Task? _delayTask = null; - private readonly List _events = new(); - private readonly Action _handleEvent; - - private readonly Action _logger; + private readonly List _events = []; + private readonly Action _handleEvent = onEvent; + private readonly Action _logger = onLogging; private long _lastEventTime = 0; private long _delayStarted = 0; private long _spamCheckStartTime = 0; private bool _spamWarningLogged = false; - - public EventProcessor(Action onEvent, Action onLogging) - { - _handleEvent = onEvent; - _logger = onLogging; - } public void ProcessEvent(FileChangedEvent fileEvent) @@ -66,7 +63,7 @@ private void HandleEventsFunc(Task _) if (_delayStarted == _lastEventTime) { // Normalize and handle - var normalized = new EventNormalizer().Normalize(_events.ToArray()); + var normalized = new EventNormalizer().Normalize([.. _events]); foreach (var ev in normalized) { _handleEvent(ev); @@ -94,7 +91,7 @@ private void WarnForSpam(FileChangedEvent fileEvent, long now) _spamWarningLogged = false; _spamCheckStartTime = now; } - else if (! _spamWarningLogged && _spamCheckStartTime + _eventSpamWarningThreshold.Ticks < now) + else if (!_spamWarningLogged && _spamCheckStartTime + _eventSpamWarningThreshold.Ticks < now) { _spamWarningLogged = true; _logger($"Warning: Watcher is busy catching up with {_events.Count} file changes " + diff --git a/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs b/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs index 05c4438..3dea57f 100644 --- a/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs +++ b/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs @@ -1,7 +1,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; -namespace FileWatcherEx; +namespace FileWatcherEx.Helpers; /// /// Interface around .NET FileSystemWatcher to be able to replace it with a fake implementation @@ -20,11 +20,11 @@ public interface IFileSystemWatcherWrapper event FileSystemEventHandler Changed; event RenamedEventHandler Renamed; event ErrorEventHandler Error; - + int InternalBufferSize { get; set; } public ISynchronizeInvoke? SynchronizingObject { get; set; } - + void Dispose(); } diff --git a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs index f943541..adb76c5 100644 --- a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs @@ -7,15 +7,34 @@ namespace FileWatcherEx.Helpers; -internal class SymlinkAwareFileWatcher : IDisposable + +/// +/// Create new instance of . +/// Object creation follows this order: +/// +/// 1) create new instance +/// 2) set properties (optional) +/// 3) call (mandatory) +/// +/// +/// Full folder path to watcher +/// onEvent callback +/// onError callback +/// how to create a FileSystemWatcher +/// logging callback +internal class SymlinkAwareFileWatcher(string path, + Action onEvent, + Action onError, + Func watcherFactory, + Action logger) : IDisposable { - private readonly string _watchPath; - private readonly Action? _eventCallback; - private readonly Action? _onError; + private readonly string _watchPath = path; + private readonly Action? _eventCallback = onEvent; + private readonly Action? _onError = onError; private Func? _getFileAttributesFunc; private Func? _getDirectoryInfosFunc; - private readonly Func _watcherFactory; - private readonly Action _logger; + private readonly Func _watcherFactory = watcherFactory; + private readonly Action _logger = logger; internal Func GetFileAttributesFunc @@ -28,13 +47,13 @@ internal Func GetDirectoryInfosFunc { get { - DirectoryInfo[] DefaultFunc(string p) => new DirectoryInfo(p).GetDirectories(); + static DirectoryInfo[] DefaultFunc(string p) => new DirectoryInfo(p).GetDirectories(); return _getDirectoryInfosFunc ?? DefaultFunc; } set => _getDirectoryInfosFunc = value; } - internal Dictionary FileWatchers { get; } = new(); + internal Dictionary FileWatchers { get; } = []; // defaults from: // https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher.notifyfilter?view=net-7.0#property-value @@ -42,44 +61,20 @@ internal Func GetDirectoryInfosFunc | NotifyFilters.FileName | NotifyFilters.DirectoryName; public bool EnableRaisingEvents { get; set; } - + public bool IncludeSubdirectories { get; set; } - - public Collection Filters { get; } = new(); - public ISynchronizeInvoke? SynchronizingObject { get; set; } + public Collection Filters { get; } = []; + public ISynchronizeInvoke? SynchronizingObject { get; set; } - /// - /// Create new instance of . - /// Object creation follows this order: - /// - /// 1) create new instance - /// 2) set properties (optional) - /// 3) call init() (mandatory) - /// - /// - /// Full folder path to watcher - /// onEvent callback - /// onError callback - /// how to create a FileSystemWatcher - /// logging callback - public SymlinkAwareFileWatcher(string path, Action onEvent, Action onError, - Func watcherFactory, Action logger) - { - _watchPath = path; - _eventCallback = onEvent; - _onError = onError; - _watcherFactory = watcherFactory; - _logger = logger; - } public void Init() { RegisterFileWatcher(_watchPath); RegisterAdditionalFileWatchersForSymLinkDirs(_watchPath); } - + private void RegisterFileWatcher(string path) { _logger($"Registering file watcher for {path}"); @@ -96,7 +91,7 @@ private void SetFileWatcherProperties(IFileSystemWatcherWrapper fileWatcher, str fileWatcher.NotifyFilter = NotifyFilter; fileWatcher.IncludeSubdirectories = IncludeSubdirectories; fileWatcher.EnableRaisingEvents = EnableRaisingEvents; - Filters.ToList().ForEach(filter => fileWatcher.Filters.Add(filter)); + Filters.ToList().ForEach(fileWatcher.Filters.Add); // currently the sync object is only registered for the root file watcher. // this preserves the old behaviour @@ -109,7 +104,7 @@ private void SetFileWatcherProperties(IFileSystemWatcherWrapper fileWatcher, str fileWatcher.InternalBufferSize = 32768; } - + private bool IsRootPath(string path) { return _watchPath == path; @@ -127,7 +122,7 @@ private void RegisterFileWatcherEventHandlers(IFileSystemWatcherWrapper fileWatc fileWatcher.Created += (_, e) => TryRegisterFileWatcherForSymbolicLinkDir(e.FullPath); fileWatcher.Deleted += UnregisterFileWatcherForSymbolicLinkDir; } - + /// /// Recursively find sym link dir and register them. /// Background: the native filewatcher does not follow symlinks so they need to be treated separately. @@ -140,7 +135,7 @@ private void RegisterAdditionalFileWatchersForSymLinkDirs(string path) { return; } - + foreach (var dirInfo in GetDirectoryInfosFunc(path)) { RegisterAdditionalFileWatchersForSymLinkDirs(dirInfo.FullName); @@ -200,9 +195,9 @@ internal void TryRegisterFileWatcherForSymbolicLinkDir(string path) /// internal void UnregisterFileWatcherForSymbolicLinkDir(object? _, FileSystemEventArgs e) { - if (FileWatchers.ContainsKey(e.FullPath)) + if (FileWatchers.TryGetValue(e.FullPath, out IFileSystemWatcherWrapper? value)) { - FileWatchers[e.FullPath].Dispose(); + value.Dispose(); FileWatchers.Remove(e.FullPath); } } @@ -218,7 +213,7 @@ private bool IsSymbolicLinkDirectory(string path) // for testing internal List GetFileWatchers() { - return FileWatchers.Values.ToList(); + return [.. FileWatchers.Values]; } diff --git a/Source/FileWatcherExTests/FileWatcherExTests.csproj b/Source/FileWatcherExTests/FileWatcherExTests.csproj index 7cd3626..3c43cea 100644 --- a/Source/FileWatcherExTests/FileWatcherExTests.csproj +++ b/Source/FileWatcherExTests/FileWatcherExTests.csproj @@ -8,15 +8,15 @@ - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs b/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs index 09a5c42..88524e3 100644 --- a/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs +++ b/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs @@ -1,8 +1,8 @@ -using System.Collections.ObjectModel; +using CsvHelper; +using FileWatcherEx.Helpers; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; -using CsvHelper; -using FileWatcherEx; namespace FileWatcherExTests; @@ -21,7 +21,7 @@ double DiffInMilliseconds /// public class ReplayFileSystemWatcherWrapper : IFileSystemWatcherWrapper { - private Collection _filters = new(); + private readonly Collection _filters = []; public void Replay(string csvFile) { @@ -37,30 +37,30 @@ public void Replay(string csvFile) switch (record.EventName) { case "created": - { - var ev = new FileSystemEventArgs(WatcherChangeTypes.Created, record.Directory, record.FileName); - Created?.Invoke(this, ev); - break; - } + { + var ev = new FileSystemEventArgs(WatcherChangeTypes.Created, record.Directory, record.FileName); + Created?.Invoke(this, ev); + break; + } case "deleted": - { - var ev = new FileSystemEventArgs(WatcherChangeTypes.Deleted, record.Directory, record.FileName); - Deleted?.Invoke(this, ev); - break; - } + { + var ev = new FileSystemEventArgs(WatcherChangeTypes.Deleted, record.Directory, record.FileName); + Deleted?.Invoke(this, ev); + break; + } case "changed": - { - var ev = new FileSystemEventArgs(WatcherChangeTypes.Changed, record.Directory, record.FileName); - Changed?.Invoke(this, ev); - break; - } + { + var ev = new FileSystemEventArgs(WatcherChangeTypes.Changed, record.Directory, record.FileName); + Changed?.Invoke(this, ev); + break; + } case "renamed": - { - var ev = new RenamedEventArgs(WatcherChangeTypes.Renamed, record.Directory, record.FileName, - record.OldFileName); - Renamed?.Invoke(this, ev); - break; - } + { + var ev = new RenamedEventArgs(WatcherChangeTypes.Renamed, record.Directory, record.FileName, + record.OldFileName); + Renamed?.Invoke(this, ev); + break; + } } } // settle down @@ -72,7 +72,7 @@ public void Replay(string csvFile) public event FileSystemEventHandler? Changed; public event RenamedEventHandler? Renamed; - #pragma warning disable CS8618 // unused in replay implementation +#pragma warning disable CS8618 // unused in replay implementation public string Path { get; set; } public Collection Filters => _filters; @@ -81,7 +81,7 @@ public void Replay(string csvFile) public bool EnableRaisingEvents { get; set; } public NotifyFilters NotifyFilter { get; set; } - #pragma warning disable CS0067 // unused in replay implementation +#pragma warning disable CS0067 // unused in replay implementation public event ErrorEventHandler? Error; public int InternalBufferSize { get; set; } public ISynchronizeInvoke? SynchronizingObject { get; set; } From 8477e13096fe93007ddc4b65eb6cefbbfa0701e9 Mon Sep 17 00:00:00 2001 From: D2 Date: Mon, 23 Feb 2026 18:42:26 +0800 Subject: [PATCH 90/92] updated to .net10 + changed namespace --- LICENSE | 2 +- README.md | 10 +- Source/Demo/Demo.WinForms.csproj | 4 +- Source/Demo/Form1.cs | 176 +++++++++--------- .../FileSystemEventRecorder.cs | 14 +- .../FileSystemEventRecorder.csproj | 4 +- Source/FileWatcherEx.sln | 43 ----- Source/FileWatcherEx.slnx | 6 + ...rEx.csproj => D2Phap.FileWatcherEx.csproj} | 11 +- Source/FileWatcherEx/FileEvents.cs | 2 +- Source/FileWatcherEx/FileSystemWatcherEx.cs | 4 +- .../FileWatcherEx/Helpers/EventNormalizer.cs | 28 ++- .../FileWatcherEx/Helpers/EventProcessor.cs | 2 +- .../Helpers/FileSystemWatcherWrapper.cs | 2 +- .../Helpers/SymlinkAwareFileWatcher.cs | 2 +- Source/FileWatcherEx/IFileSystemWatcherEx.cs | 2 +- .../FileWatcherExTests/EventNormalizerTest.cs | 6 +- .../FileWatcherExIntegrationTest.cs | 86 +++++---- .../FileWatcherExTests.csproj | 4 +- .../ReplayFileSystemWatcherWrapper.cs | 2 +- .../SymlinkAwareFileWatcherTest.cs | 61 +++--- 21 files changed, 223 insertions(+), 248 deletions(-) delete mode 100644 Source/FileWatcherEx.sln create mode 100644 Source/FileWatcherEx.slnx rename Source/FileWatcherEx/{FileWatcherEx.csproj => D2Phap.FileWatcherEx.csproj} (83%) diff --git a/LICENSE b/LICENSE index 15f27b7..0447c45 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2025 Duong Dieu Phap +Copyright (c) 2018-2026 Duong Dieu Phap Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a809262..7a5c4be 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FileWatcherEx for Windows +# D2Phap.FileWatcherEx for Windows A wrapper of `System.IO.FileSystemWatcher` to standardize the events and avoid false change notifications. It has been being used in [ImageGlass - A lightweight, versatile image viewer](https://github.com/d2phap/ImageGlass) project. This project is based on the *VSCode FileWatcher*: https://github.com/Microsoft/vscode-filewatcher-windows @@ -7,28 +7,28 @@ This project is based on the *VSCode FileWatcher*: https://github.com/Microsoft/ ## Resource links -- Nuget package: [https://www.nuget.org/packages/FileWatcherEx](https://www.nuget.org/packages/FileWatcherEx/) +- Nuget package: [https://www.nuget.org/packages/D2Phap.FileWatcherEx](https://www.nuget.org/packages/D2Phap.FileWatcherEx/) - Project url: [https://github.com/d2phap/FileWatcherEx](https://github.com/d2phap/FileWatcherEx) - Website: [https://imageglass.org](https://imageglass.org) ## Features - Standardizes the events of `System.IO.FileSystemWatcher`. - No false change notifications when a file system item is created, deleted, changed or renamed. -- Supports .NET 6.0, 7.0, 8.0, 9.0 +- Supports .NET 8.0, 10.0 ## Installation Run the command: ```bash # Nuget package -Install-Package FileWatcherEx +Install-Package D2Phap.FileWatcherEx ``` ## Usage See Demo project for full details! ```cs -using FileWatcherEx; +using D2Phap.FileWatcherEx; var _fw = new FileSystemWatcherEx(@"C:\path\to\watch"); diff --git a/Source/Demo/Demo.WinForms.csproj b/Source/Demo/Demo.WinForms.csproj index 89ebc04..74245b7 100644 --- a/Source/Demo/Demo.WinForms.csproj +++ b/Source/Demo/Demo.WinForms.csproj @@ -2,14 +2,14 @@ WinExe - net9.0-windows + net10.0-windows enable true enable - + \ No newline at end of file diff --git a/Source/Demo/Form1.cs b/Source/Demo/Form1.cs index 92a035a..5f1388b 100644 --- a/Source/Demo/Form1.cs +++ b/Source/Demo/Form1.cs @@ -1,122 +1,120 @@ +using D2Phap.FileWatcherEx; -using FileWatcherEx; +namespace Demo; -namespace Demo +public partial class Form1 : Form { - public partial class Form1 : Form + private FileSystemWatcherEx _fw = new(); + + public Form1() { - private FileSystemWatcherEx _fw = new(); + InitializeComponent(); + } - public Form1() - { - InitializeComponent(); - } + private void BtnStart_Click(object sender, EventArgs e) + { + _fw = new FileSystemWatcherEx(txtPath.Text.Trim(), FW_OnLog); - private void BtnStart_Click(object sender, EventArgs e) - { - _fw = new FileSystemWatcherEx(txtPath.Text.Trim(), FW_OnLog); - - _fw.OnRenamed += FW_OnRenamed; - _fw.OnCreated += FW_OnCreated; - _fw.OnDeleted += FW_OnDeleted; - _fw.OnChanged += FW_OnChanged; - _fw.OnError += FW_OnError; - - _fw.SynchronizingObject = this; - _fw.IncludeSubdirectories = true; - - try - { - _fw.Start(); - - btnStart.Enabled = true; - btnSelectFolder.Enabled = false; - txtPath.Enabled = false; - btnStop.Enabled = true; - } - catch (Exception ex) - { - MessageBox.Show(ex.Message); - } - } + _fw.OnRenamed += FW_OnRenamed; + _fw.OnCreated += FW_OnCreated; + _fw.OnDeleted += FW_OnDeleted; + _fw.OnChanged += FW_OnChanged; + _fw.OnError += FW_OnError; - private void FW_OnError(object? sender, ErrorEventArgs e) - { - if (txtConsole.InvokeRequired) - { - txtConsole.Invoke(FW_OnError, sender, e); - } - else - { - txtConsole.Text += "[ERROR]: " + e.GetException().Message + "\r\n"; - } - } + _fw.SynchronizingObject = this; + _fw.IncludeSubdirectories = true; - private void FW_OnChanged(object? sender, FileChangedEvent e) + try { - txtConsole.Text += string.Format("[cha] {0} | {1}", - Enum.GetName(typeof(ChangeType), e.ChangeType), - e.FullPath) + "\r\n"; - } + _fw.Start(); - private void FW_OnDeleted(object? sender, FileChangedEvent e) - { - txtConsole.Text += string.Format("[del] {0} | {1}", - Enum.GetName(typeof(ChangeType), e.ChangeType), - e.FullPath) + "\r\n"; + btnStart.Enabled = true; + btnSelectFolder.Enabled = false; + txtPath.Enabled = false; + btnStop.Enabled = true; } - - private void FW_OnCreated(object? sender, FileChangedEvent e) + catch (Exception ex) { - txtConsole.Text += string.Format("[cre] {0} | {1}", - Enum.GetName(typeof(ChangeType), e.ChangeType), - e.FullPath) + "\r\n"; + MessageBox.Show(ex.Message); } + } - private void FW_OnRenamed(object? sender, FileChangedEvent e) + private void FW_OnError(object? sender, ErrorEventArgs e) + { + if (txtConsole.InvokeRequired) { - txtConsole.Text += string.Format("[ren] {0} | {1} ----> {2}", - Enum.GetName(typeof(ChangeType), e.ChangeType), - e.OldFullPath, - e.FullPath) + "\r\n"; + txtConsole.Invoke(FW_OnError, sender, e); } - - private void FW_OnLog(string value) + else { - txtConsole.Text += $@"[log] {value}" + "\r\n"; + txtConsole.Text += "[ERROR]: " + e.GetException().Message + "\r\n"; } + } - private void BtnStop_Click(object sender, EventArgs e) - { - _fw.Stop(); + private void FW_OnChanged(object? sender, FileChangedEvent e) + { + txtConsole.Text += string.Format("[cha] {0} | {1}", + Enum.GetName(e.ChangeType), + e.FullPath) + "\r\n"; + } - btnStart.Enabled = true; - btnSelectFolder.Enabled = true; - txtPath.Enabled = true; - btnStop.Enabled = false; - btnStop.Enabled = true; - } + private void FW_OnDeleted(object? sender, FileChangedEvent e) + { + txtConsole.Text += string.Format("[del] {0} | {1}", + Enum.GetName(e.ChangeType), + e.FullPath) + "\r\n"; + } + private void FW_OnCreated(object? sender, FileChangedEvent e) + { + txtConsole.Text += string.Format("[cre] {0} | {1}", + Enum.GetName(e.ChangeType), + e.FullPath) + "\r\n"; + } - private void BtnSelectFolder_Click(object sender, EventArgs e) - { - var fb = new FolderBrowserDialog(); + private void FW_OnRenamed(object? sender, FileChangedEvent e) + { + txtConsole.Text += string.Format("[ren] {0} | {1} ----> {2}", + Enum.GetName(e.ChangeType), + e.OldFullPath, + e.FullPath) + "\r\n"; + } + private void FW_OnLog(string value) + { + txtConsole.Text += $@"[log] {value}" + "\r\n"; + } - if (fb.ShowDialog() == DialogResult.OK) - { - txtPath.Text = fb.SelectedPath; + private void BtnStop_Click(object sender, EventArgs e) + { + _fw.Stop(); + + btnStart.Enabled = true; + btnSelectFolder.Enabled = true; + txtPath.Enabled = true; + btnStop.Enabled = false; + btnStop.Enabled = true; + } + + + private void BtnSelectFolder_Click(object sender, EventArgs e) + { + var fb = new FolderBrowserDialog(); - _fw.Stop(); - _fw.Dispose(); - } - } - private void Form1_FormClosing(object sender, FormClosingEventArgs e) + if (fb.ShowDialog() == DialogResult.OK) { + txtPath.Text = fb.SelectedPath; + + _fw.Stop(); _fw.Dispose(); } } + + private void Form1_FormClosing(object sender, FormClosingEventArgs e) + { + _fw.Dispose(); + } } \ No newline at end of file diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs index 45a43f8..be5fe3e 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs @@ -35,12 +35,14 @@ public static void Main(string[] args) { var (watchedDirectory, csvOutputFile) = ProcessArguments(args); - var watcher = new FileSystemWatcher(); - watcher.Path = watchedDirectory; - watcher.IncludeSubdirectories = true; - watcher.NotifyFilter = NotifyFilters.LastWrite - | NotifyFilters.FileName - | NotifyFilters.DirectoryName; + var watcher = new FileSystemWatcher + { + Path = watchedDirectory, + IncludeSubdirectories = true, + NotifyFilter = NotifyFilters.LastWrite + | NotifyFilters.FileName + | NotifyFilters.DirectoryName + }; watcher.Created += (_, ev) => EventRecords.Enqueue(new EventRecord(ev.FullPath, "created", null, Stopwatch.GetTimestamp())); diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj b/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj index 936cf26..6dcb059 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj @@ -2,13 +2,13 @@ Exe - net9.0 + net10.0 enable enable - + diff --git a/Source/FileWatcherEx.sln b/Source/FileWatcherEx.sln deleted file mode 100644 index abd9b7d..0000000 --- a/Source/FileWatcherEx.sln +++ /dev/null @@ -1,43 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.1.31911.260 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileWatcherEx", "FileWatcherEx\FileWatcherEx.csproj", "{CF81727D-B6EC-4202-8E78-087C5D5EABF3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo.WinForms", "Demo\Demo.WinForms.csproj", "{F973F462-7769-433C-AEC1-17AF2902ED2D}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileWatcherExTests", "FileWatcherExTests\FileWatcherExTests.csproj", "{1C0CA67C-369E-4258-B661-2C545B50A6FF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystemEventRecorder", "FileSystemEventRecorder\FileSystemEventRecorder.csproj", "{F87993D7-2487-41BD-9044-EBEB54BAD13C}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {CF81727D-B6EC-4202-8E78-087C5D5EABF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {CF81727D-B6EC-4202-8E78-087C5D5EABF3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {CF81727D-B6EC-4202-8E78-087C5D5EABF3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {CF81727D-B6EC-4202-8E78-087C5D5EABF3}.Release|Any CPU.Build.0 = Release|Any CPU - {F973F462-7769-433C-AEC1-17AF2902ED2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F973F462-7769-433C-AEC1-17AF2902ED2D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F973F462-7769-433C-AEC1-17AF2902ED2D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F973F462-7769-433C-AEC1-17AF2902ED2D}.Release|Any CPU.Build.0 = Release|Any CPU - {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Release|Any CPU.Build.0 = Release|Any CPU - {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {7A615E75-F056-422F-952E-9C0CD34E477B} - EndGlobalSection -EndGlobal diff --git a/Source/FileWatcherEx.slnx b/Source/FileWatcherEx.slnx new file mode 100644 index 0000000..1edf624 --- /dev/null +++ b/Source/FileWatcherEx.slnx @@ -0,0 +1,6 @@ + + + + + + diff --git a/Source/FileWatcherEx/FileWatcherEx.csproj b/Source/FileWatcherEx/D2Phap.FileWatcherEx.csproj similarity index 83% rename from Source/FileWatcherEx/FileWatcherEx.csproj rename to Source/FileWatcherEx/D2Phap.FileWatcherEx.csproj index afa2b55..353eff7 100644 --- a/Source/FileWatcherEx/FileWatcherEx.csproj +++ b/Source/FileWatcherEx/D2Phap.FileWatcherEx.csproj @@ -1,10 +1,10 @@ - net6.0;net7.0;net8.0;net9.0 + net8.0;net10.0 enable enable - Copyright © 2018-2025 Duong Dieu Phap + Copyright © 2018-2026 Duong Dieu Phap https://github.com/d2phap/FileWatcherEx README.md https://github.com/d2phap/FileWatcherEx @@ -13,7 +13,7 @@ A wrapper of FileSystemWatcher to standardize the events and avoid false change notifications, used in ImageGlass project (https://imageglass.org). This project is based on the VSCode FileWatcher: https://github.com/Microsoft/vscode-filewatcher-windows. True $(Version) - 2.7.0 + 3.0.0 See https://github.com/d2phap/FileWatcherEx/releases d2phap FileWatcherEx - A file system watcher @@ -22,6 +22,11 @@ LICENSE False latest + + true + true + true + false diff --git a/Source/FileWatcherEx/FileEvents.cs b/Source/FileWatcherEx/FileEvents.cs index 1ce5d5d..871dcbb 100644 --- a/Source/FileWatcherEx/FileEvents.cs +++ b/Source/FileWatcherEx/FileEvents.cs @@ -1,4 +1,4 @@ -namespace FileWatcherEx; +namespace D2Phap.FileWatcherEx; public enum ChangeType { diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/FileWatcherEx/FileSystemWatcherEx.cs index aa14292..e382dd3 100644 --- a/Source/FileWatcherEx/FileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/FileSystemWatcherEx.cs @@ -1,9 +1,9 @@  -using FileWatcherEx.Helpers; +using D2Phap.FileWatcherEx.Helpers; using System.Collections.Concurrent; using System.ComponentModel; -namespace FileWatcherEx; +namespace D2Phap.FileWatcherEx; /// diff --git a/Source/FileWatcherEx/Helpers/EventNormalizer.cs b/Source/FileWatcherEx/Helpers/EventNormalizer.cs index bb7a37a..77fc675 100644 --- a/Source/FileWatcherEx/Helpers/EventNormalizer.cs +++ b/Source/FileWatcherEx/Helpers/EventNormalizer.cs @@ -1,6 +1,4 @@ -using static FileWatcherEx.ChangeType; - -namespace FileWatcherEx.Helpers; +namespace D2Phap.FileWatcherEx.Helpers; /// /// Tries to fix the real life oddities of the underlying FileSystemWatcher class. @@ -23,25 +21,25 @@ private void NormalizeDuplicates(FileChangedEvent[] events) { var oldEvent = _eventRepo.Find(newEvent.FullPath); // original file event from which we renamed, only applicable for RENAMED event - var renameFromEvent = newEvent.ChangeType == RENAMED + var renameFromEvent = newEvent.ChangeType == ChangeType.RENAMED ? _eventRepo.Find(newEvent.OldFullPath) : null; switch (newEvent.ChangeType) { // CREATED followed by CHANGED => CREATED - case CHANGED when oldEvent?.ChangeType == CREATED: + case ChangeType.CHANGED when oldEvent?.ChangeType == ChangeType.CREATED: // Do nothing break; // CREATED followed by DELETED => remove - case DELETED when oldEvent?.ChangeType == CREATED: + case ChangeType.DELETED when oldEvent?.ChangeType == ChangeType.CREATED: _eventRepo.Remove(oldEvent); break; // DELETED followed by CREATED => CHANGED - case CREATED when oldEvent?.ChangeType == DELETED: - oldEvent.ChangeType = CHANGED; + case ChangeType.CREATED when oldEvent?.ChangeType == ChangeType.DELETED: + oldEvent.ChangeType = ChangeType.CHANGED; break; // Scenario: @@ -49,8 +47,8 @@ private void NormalizeDuplicates(FileChangedEvent[] events) // - file bar is deleted // - now foo is renamed to the just deleted bar // - this results into a bar changed event - case RENAMED when oldEvent?.ChangeType == DELETED && renameFromEvent?.ChangeType == CREATED: - newEvent.ChangeType = CHANGED; + case ChangeType.RENAMED when oldEvent?.ChangeType == ChangeType.DELETED && renameFromEvent?.ChangeType == ChangeType.CREATED: + newEvent.ChangeType = ChangeType.CHANGED; newEvent.OldFullPath = null; _eventRepo.AddOrUpdate(newEvent); @@ -59,8 +57,8 @@ private void NormalizeDuplicates(FileChangedEvent[] events) break; // rename from CREATED file, all other cases - case RENAMED when renameFromEvent?.ChangeType == CREATED: - newEvent.ChangeType = CREATED; + case ChangeType.RENAMED when renameFromEvent?.ChangeType == ChangeType.CREATED: + newEvent.ChangeType = ChangeType.CREATED; newEvent.OldFullPath = null; _eventRepo.AddOrUpdate(newEvent); @@ -68,7 +66,7 @@ private void NormalizeDuplicates(FileChangedEvent[] events) _eventRepo.Remove(renameFromEvent); break; - case RENAMED when renameFromEvent?.ChangeType == RENAMED: + case ChangeType.RENAMED when renameFromEvent?.ChangeType == ChangeType.RENAMED: newEvent.OldFullPath = renameFromEvent.OldFullPath; _eventRepo.AddOrUpdate(newEvent); @@ -78,7 +76,7 @@ private void NormalizeDuplicates(FileChangedEvent[] events) // the LOG event is not coming from the filesystem, hence it is ignored. // ideally, LOG would disappear completely but unfortunately it is part of the public API of this lib - case LOG: + case ChangeType.LOG: // ignore break; @@ -110,7 +108,7 @@ internal static IEnumerable FilterDeleted(IEnumerable deletedPaths) { - if (e.ChangeType == DELETED) + if (e.ChangeType == ChangeType.DELETED) { if (deletedPaths.Any(d => IsParent(e.FullPath, d))) { diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/FileWatcherEx/Helpers/EventProcessor.cs index 3232066..e661f29 100644 --- a/Source/FileWatcherEx/Helpers/EventProcessor.cs +++ b/Source/FileWatcherEx/Helpers/EventProcessor.cs @@ -2,7 +2,7 @@ * Copyright (C) Microsoft Corporation. All rights reserved. *--------------------------------------------------------*/ -namespace FileWatcherEx.Helpers; +namespace D2Phap.FileWatcherEx.Helpers; internal class EventProcessor(Action onEvent, Action onLogging) { diff --git a/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs b/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs index 3dea57f..5fa8b82 100644 --- a/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs +++ b/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs @@ -1,7 +1,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; -namespace FileWatcherEx.Helpers; +namespace D2Phap.FileWatcherEx.Helpers; /// /// Interface around .NET FileSystemWatcher to be able to replace it with a fake implementation diff --git a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs index adb76c5..033bdec 100644 --- a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs +++ b/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs @@ -5,7 +5,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; -namespace FileWatcherEx.Helpers; +namespace D2Phap.FileWatcherEx.Helpers; /// diff --git a/Source/FileWatcherEx/IFileSystemWatcherEx.cs b/Source/FileWatcherEx/IFileSystemWatcherEx.cs index 8da53ed..b794171 100644 --- a/Source/FileWatcherEx/IFileSystemWatcherEx.cs +++ b/Source/FileWatcherEx/IFileSystemWatcherEx.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace FileWatcherEx; +namespace D2Phap.FileWatcherEx; public interface IFileSystemWatcherEx { diff --git a/Source/FileWatcherExTests/EventNormalizerTest.cs b/Source/FileWatcherExTests/EventNormalizerTest.cs index 44e2e5a..3a0b97d 100644 --- a/Source/FileWatcherExTests/EventNormalizerTest.cs +++ b/Source/FileWatcherExTests/EventNormalizerTest.cs @@ -1,6 +1,6 @@ +using D2Phap.FileWatcherEx; +using D2Phap.FileWatcherEx.Helpers; using Xunit; -using FileWatcherEx; -using FileWatcherEx.Helpers; namespace FileWatcherExTests; @@ -188,7 +188,7 @@ public void Created_Event_After_Deleted_Results_Into_Changed() Assert.Equal(@"c:\foo", ev.FullPath); Assert.Null(ev.OldFullPath); } - + [Fact] public void Changed_Event_After_Created_Is_Ignored() { diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index cdf5d8d..300e488 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -1,6 +1,6 @@ -using System.Collections.Concurrent; -using FileWatcherEx; +using D2Phap.FileWatcherEx; using FileWatcherExTests.Helper; +using System.Collections.Concurrent; using Xunit; using Xunit.Abstractions; @@ -22,11 +22,13 @@ public FileWatcherExIntegrationTest(ITestOutputHelper testOutputHelper) // setup before each test run _events = new ConcurrentQueue(); _replayFileSystemWatcherFactory = new ReplayFileSystemWatcherFactory(); - + _tempDir = new TempDir(); - _fileWatcher = new FileSystemWatcherEx(_tempDir.FullPath, testOutputHelper.WriteLine); - _fileWatcher.FileSystemWatcherFactory = () => _replayFileSystemWatcherFactory.Create(); - _fileWatcher.IncludeSubdirectories = true; + _fileWatcher = new FileSystemWatcherEx(_tempDir.FullPath, testOutputHelper.WriteLine) + { + FileSystemWatcherFactory = () => _replayFileSystemWatcherFactory.Create(), + IncludeSubdirectories = true + }; _fileWatcher.OnCreated += (_, ev) => _events.Enqueue(ev); _fileWatcher.OnDeleted += (_, ev) => _events.Enqueue(ev); @@ -34,7 +36,7 @@ public FileWatcherExIntegrationTest(ITestOutputHelper testOutputHelper) _fileWatcher.OnRenamed += (_, ev) => _events.Enqueue(ev); } - + [Fact] public void Create_Single_File() { @@ -46,7 +48,7 @@ public void Create_Single_File() AssertEqualNormalized(@"C:\temp\fwtest\a.txt", ev.FullPath); Assert.Equal("", ev.OldFullPath); } - + [Fact] public void Create_And_Remove_Single_File() @@ -89,7 +91,7 @@ public void Create_Rename_And_Remove_Single_File() Assert.Equal("", ev3.OldFullPath); } - + [Fact] // filters out 2nd "changed" event public void Create_Single_File_Via_WSL2() @@ -102,8 +104,8 @@ public void Create_Single_File_Via_WSL2() AssertEqualNormalized(@"C:\temp\fwtest\a.txt", ev.FullPath); Assert.Equal("", ev.OldFullPath); } - - + + [Fact] // scenario creates "created" "changed" and "renamed" event. // resulting event is just "created" with the filename taken from "renamed" @@ -118,7 +120,7 @@ public void Create_And_Rename_Single_File_Via_WSL2() Assert.Null(ev.OldFullPath); } - + [Fact] public void Create_Rename_And_Remove_Single_File_Via_WSL2() { @@ -126,7 +128,7 @@ public void Create_Rename_And_Remove_Single_File_Via_WSL2() Assert.Empty(_events); } - + [Fact] public void Create_Rename_And_Remove_Single_File_With_Wait_Time_Via_WSL2() { @@ -148,8 +150,8 @@ public void Create_Rename_And_Remove_Single_File_With_Wait_Time_Via_WSL2() AssertEqualNormalized(@"C:\temp\fwtest\b.txt", ev3.FullPath); Assert.Equal("", ev3.OldFullPath); } - - + + [Fact] public void Manually_Create_And_Rename_File_Via_Windows_Explorer() { @@ -201,7 +203,7 @@ public void Download_Image_Via_Edge_Browser() Assert.Equal(ChangeType.CREATED, ev1.ChangeType); AssertEqualNormalized(@"C:\temp\fwtest\test.png.crdownload", ev1.FullPath); - + Assert.Equal(ChangeType.RENAMED, ev2.ChangeType); AssertEqualNormalized(@"C:\temp\fwtest\test.png", ev2.FullPath); AssertEqualNormalized(@"C:\temp\fwtest\test.png.crdownload", ev2.OldFullPath); @@ -216,10 +218,10 @@ public void Create_Sub_Directory_Add_And_Remove_File() Assert.Equal(2, _events.Count); var ev1 = _events.ToList()[0]; var ev2 = _events.ToList()[1]; - + Assert.Equal(ChangeType.CREATED, ev1.ChangeType); AssertEqualNormalized(@"C:\temp\fwtest\subdir", ev1.FullPath); - + Assert.Equal(ChangeType.CHANGED, ev2.ChangeType); AssertEqualNormalized(@"C:\temp\fwtest\subdir", ev2.FullPath); Assert.Equal(@"", ev2.OldFullPath); @@ -235,10 +237,10 @@ public void Create_Sub_Directory_Add_And_Remove_File_With_Sleep() var ev2 = _events.ToList()[1]; var ev3 = _events.ToList()[2]; var ev4 = _events.ToList()[3]; - + Assert.Equal(ChangeType.CREATED, ev1.ChangeType); AssertEqualNormalized(@"C:\temp\fwtest\subdir", ev1.FullPath); - + Assert.Equal(ChangeType.CREATED, ev2.ChangeType); AssertEqualNormalized(@"C:\temp\fwtest\subdir\a.txt", ev2.FullPath); Assert.Equal(@"", ev2.OldFullPath); @@ -258,25 +260,29 @@ public void Filter_Settings_Are_Delegated() { using var dir = new TempDir(); var watcher = new ReplayFileSystemWatcherWrapper(); - - var uut = new FileSystemWatcherEx(dir.FullPath); - uut.FileSystemWatcherFactory = () => watcher; + + var uut = new FileSystemWatcherEx(dir.FullPath) + { + FileSystemWatcherFactory = () => watcher + }; uut.Filters.Add("*.foo"); uut.Filters.Add("*.bar"); - + uut.Start(); - Assert.Equal(new List{"*.foo", "*.bar"}, watcher.Filters); + Assert.Equal(new List { "*.foo", "*.bar" }, watcher.Filters); } - + [Fact] public void Set_Filter() { using var dir = new TempDir(); var watcher = new ReplayFileSystemWatcherWrapper(); - - var uut = new FileSystemWatcherEx(dir.FullPath); - uut.FileSystemWatcherFactory = () => watcher; - + + var uut = new FileSystemWatcherEx(dir.FullPath) + { + FileSystemWatcherFactory = () => watcher + }; + // "all files" by default Assert.Equal("*", uut.Filter); @@ -285,7 +291,7 @@ public void Set_Filter() // two filter entries Assert.Equal(2, uut.Filters.Count); - + // if multiple filters, only first is displayed. TODO Why ? Assert.Equal("*.foo", uut.Filter); @@ -294,8 +300,8 @@ public void Set_Filter() Assert.Single(uut.Filters); } - - + + [Fact(Skip = "requires real (Windows) file system")] public void Simple_Real_File_System_Test() { @@ -315,8 +321,8 @@ public void Simple_Real_File_System_Test() } _fileWatcher.StartForTesting( - p => FileAttributes.Normal, - p => Array.Empty()); + p => FileAttributes.Normal, + p => []); File.Create(testFile); Thread.Sleep(250); fw.Stop(); @@ -327,7 +333,7 @@ public void Simple_Real_File_System_Test() Assert.Equal(@"c:\temp\fwtest\b.txt", ev.FullPath); Assert.Equal("", ev.OldFullPath); } - + // cleanup public void Dispose() { @@ -340,14 +346,14 @@ private void StartFileWatcherAndReplay(string csvFile) _fileWatcher.StartForTesting( p => FileAttributes.Normal, // only used for FullName - p => new[] { new DirectoryInfo(p)}); + p => [new DirectoryInfo(p)]); _replayFileSystemWatcherFactory.RootWatcher.Replay(csvFile); _fileWatcher.Stop(); } - + private class ReplayFileSystemWatcherFactory { - private readonly List _wrappers = new(); + private readonly List _wrappers = []; public ReplayFileSystemWatcherWrapper Create() { @@ -360,7 +366,7 @@ public ReplayFileSystemWatcherWrapper Create() // This is the one which is registered first and watches the root directory. public ReplayFileSystemWatcherWrapper RootWatcher => _wrappers[0]; } - + // little hack to make the path comparision platform independent private static void AssertEqualNormalized(string expected, string? actual) { diff --git a/Source/FileWatcherExTests/FileWatcherExTests.csproj b/Source/FileWatcherExTests/FileWatcherExTests.csproj index 3c43cea..0441516 100644 --- a/Source/FileWatcherExTests/FileWatcherExTests.csproj +++ b/Source/FileWatcherExTests/FileWatcherExTests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable false @@ -23,7 +23,7 @@ - + diff --git a/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs b/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs index 88524e3..39be8dc 100644 --- a/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs +++ b/Source/FileWatcherExTests/ReplayFileSystemWatcherWrapper.cs @@ -1,5 +1,5 @@ using CsvHelper; -using FileWatcherEx.Helpers; +using D2Phap.FileWatcherEx.Helpers; using System.Collections.ObjectModel; using System.ComponentModel; using System.Globalization; diff --git a/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs b/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs index d146e31..cc6d57d 100644 --- a/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs +++ b/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs @@ -1,9 +1,8 @@ -using System.Collections.ObjectModel; -using System.ComponentModel; -using FileWatcherEx; -using FileWatcherEx.Helpers; +using D2Phap.FileWatcherEx.Helpers; using FileWatcherExTests.Helper; using Moq; +using System.Collections.ObjectModel; +using System.ComponentModel; using Xunit; using Xunit.Abstractions; @@ -17,7 +16,7 @@ public class SymlinkAwareFileWatcherTest public SymlinkAwareFileWatcherTest(ITestOutputHelper testOutputHelper) { _testOutputHelper = testOutputHelper; - _mocks = new List>(); + _mocks = []; } [Fact] @@ -51,7 +50,7 @@ public void FileWatchers_For_SymLink_Dirs_Are_Created_On_Startup() var symlinkPath1 = dir.CreateSymlink(symLink: "sym1", target: subdirPath1); // symlink {tempdir}/sym1/sym2 to {tempdir}/subdir2 - var symlinkPath2 = dir.CreateSymlink(symLink: new []{"sym1", "sym2"}, target: subdirPath2); + var symlinkPath2 = dir.CreateSymlink(symLink: ["sym1", "sym2"], target: subdirPath2); CreateFileWatcher(dir.FullPath); @@ -116,17 +115,18 @@ public void Properties_Are_Propagated() // symlink for detection at startup dir.CreateSymlink( symLink: "sym1", - target: subDir); + target: subDir); var uut = new SymlinkAwareFileWatcher(dir.FullPath, _ => { }, _ => { }, WatcherFactoryWithMemory, - _ => { }); - - // perform settings. all, except SynchronizingObject are propagated - // to all registered watchers - uut.NotifyFilter = NotifyFilters.LastAccess; + _ => { }) + { + // perform settings. all, except SynchronizingObject are propagated + // to all registered watchers + NotifyFilter = NotifyFilters.LastAccess + }; uut.Filters.Add("*.foo"); uut.Filters.Add("*.bar"); uut.EnableRaisingEvents = true; @@ -136,15 +136,15 @@ public void Properties_Are_Propagated() // finish object initialization uut.Init(); - + // create symlink at runtime var symlinkPath2 = dir.CreateSymlink( symLink: "sym2", - target: subDir); + target: subDir); // simulate that a new symlink dir was added uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath2); - + // 1x root watcher, 1x sym link at startup, 1x sym link at runtime Assert.Equal(3, _mocks.Count); // all watchers have properties set @@ -161,11 +161,11 @@ public void Properties_Are_Propagated() mock => mock.VerifySet(w => w.IncludeSubdirectories = true)); Assert.All( - _mocks, - mock => Assert.Equal(mock.Object.Filters, new Collection { "*.foo", "*.bar" })); + _mocks, + mock => Assert.Equal(mock.Object.Filters, ["*.foo", "*.bar"])); // sync. object is only set for root watcher - Assert.Collection(_mocks, + Assert.Collection(_mocks, rootWatcherMock => { rootWatcherMock.VerifySet(w => w.Path = dir.FullPath); @@ -175,7 +175,7 @@ public void Properties_Are_Propagated() otherWatcherMock => otherWatcherMock.VerifySet(w => w.SynchronizingObject = syncObj, Times.Never)); } - + [Fact] public void When_No_SubDirs_Are_Watched_Also_No_Additional_Symlink_Watchers_Are_Registered() { @@ -186,38 +186,41 @@ public void When_No_SubDirs_Are_Watched_Also_No_Additional_Symlink_Watchers_Are_ // symlink for detection at startup dir.CreateSymlink( symLink: "sym1", - target: subDir); + target: subDir); var uut = new SymlinkAwareFileWatcher(dir.FullPath, _ => { }, _ => { }, WatcherFactoryWithMemory, - _ => { }); - - uut.IncludeSubdirectories = false; + _ => { }) + { + IncludeSubdirectories = false + }; uut.Init(); - + // create symlink at runtime var symlinkPath2 = dir.CreateSymlink( symLink: "sym2", - target: subDir); + target: subDir); // simulate that a new symlink dir was added uut.TryRegisterFileWatcherForSymbolicLinkDir(symlinkPath2); - + // only root watcher was registered Assert.Single(_mocks); } - + private SymlinkAwareFileWatcher CreateFileWatcher(string path) { var fw = new SymlinkAwareFileWatcher(path, _ => { }, _ => { }, WatcherFactoryWithMemory, - _ => { }); - fw.IncludeSubdirectories = true; + _ => { }) + { + IncludeSubdirectories = true + }; fw.Init(); return fw; } From 1684ab2049e9272083f7edba0d06b7b4767b92dc Mon Sep 17 00:00:00 2001 From: D2 Date: Mon, 23 Feb 2026 18:49:22 +0800 Subject: [PATCH 91/92] updated nuget dependencies --- .../FileWatcherExTests/FileWatcherExIntegrationTest.cs | 1 - Source/FileWatcherExTests/FileWatcherExTests.csproj | 9 +++++---- Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs index 300e488..faa52bf 100644 --- a/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs +++ b/Source/FileWatcherExTests/FileWatcherExIntegrationTest.cs @@ -2,7 +2,6 @@ using FileWatcherExTests.Helper; using System.Collections.Concurrent; using Xunit; -using Xunit.Abstractions; namespace FileWatcherExTests; diff --git a/Source/FileWatcherExTests/FileWatcherExTests.csproj b/Source/FileWatcherExTests/FileWatcherExTests.csproj index 0441516..12b59c3 100644 --- a/Source/FileWatcherExTests/FileWatcherExTests.csproj +++ b/Source/FileWatcherExTests/FileWatcherExTests.csproj @@ -9,17 +9,18 @@ - + + - - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all + diff --git a/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs b/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs index cc6d57d..b01c39a 100644 --- a/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs +++ b/Source/FileWatcherExTests/SymlinkAwareFileWatcherTest.cs @@ -4,7 +4,6 @@ using System.Collections.ObjectModel; using System.ComponentModel; using Xunit; -using Xunit.Abstractions; namespace FileWatcherExTests; From ef0a855917f650cc46ede7c775ecd477d1c6ee5d Mon Sep 17 00:00:00 2001 From: D2 Date: Mon, 23 Feb 2026 19:00:35 +0800 Subject: [PATCH 92/92] updated project folder name --- .../D2Phap.FileWatcherEx.csproj | 3 ++- Source/{FileWatcherEx => D2Phap.FileWatcherEx}/FileEvents.cs | 0 .../FileSystemWatcherEx.cs | 0 .../Helpers/EventNormalizer.cs | 0 .../Helpers/EventProcessor.cs | 0 .../Helpers/FileSystemWatcherWrapper.cs | 0 .../Helpers/SymlinkAwareFileWatcher.cs | 0 .../IFileSystemWatcherEx.cs | 0 Source/Demo/Demo.WinForms.csproj | 2 +- Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj | 2 +- Source/FileWatcherEx.slnx | 2 +- Source/FileWatcherExTests/FileWatcherExTests.csproj | 2 +- 12 files changed, 6 insertions(+), 5 deletions(-) rename Source/{FileWatcherEx => D2Phap.FileWatcherEx}/D2Phap.FileWatcherEx.csproj (95%) rename Source/{FileWatcherEx => D2Phap.FileWatcherEx}/FileEvents.cs (100%) rename Source/{FileWatcherEx => D2Phap.FileWatcherEx}/FileSystemWatcherEx.cs (100%) rename Source/{FileWatcherEx => D2Phap.FileWatcherEx}/Helpers/EventNormalizer.cs (100%) rename Source/{FileWatcherEx => D2Phap.FileWatcherEx}/Helpers/EventProcessor.cs (100%) rename Source/{FileWatcherEx => D2Phap.FileWatcherEx}/Helpers/FileSystemWatcherWrapper.cs (100%) rename Source/{FileWatcherEx => D2Phap.FileWatcherEx}/Helpers/SymlinkAwareFileWatcher.cs (100%) rename Source/{FileWatcherEx => D2Phap.FileWatcherEx}/IFileSystemWatcherEx.cs (100%) diff --git a/Source/FileWatcherEx/D2Phap.FileWatcherEx.csproj b/Source/D2Phap.FileWatcherEx/D2Phap.FileWatcherEx.csproj similarity index 95% rename from Source/FileWatcherEx/D2Phap.FileWatcherEx.csproj rename to Source/D2Phap.FileWatcherEx/D2Phap.FileWatcherEx.csproj index 353eff7..bf555e3 100644 --- a/Source/FileWatcherEx/D2Phap.FileWatcherEx.csproj +++ b/Source/D2Phap.FileWatcherEx/D2Phap.FileWatcherEx.csproj @@ -16,7 +16,7 @@ 3.0.0 See https://github.com/d2phap/FileWatcherEx/releases d2phap - FileWatcherEx - A file system watcher + D2Phap.FileWatcherEx - A file system watcher True snupkg LICENSE @@ -27,6 +27,7 @@ true true false + latest diff --git a/Source/FileWatcherEx/FileEvents.cs b/Source/D2Phap.FileWatcherEx/FileEvents.cs similarity index 100% rename from Source/FileWatcherEx/FileEvents.cs rename to Source/D2Phap.FileWatcherEx/FileEvents.cs diff --git a/Source/FileWatcherEx/FileSystemWatcherEx.cs b/Source/D2Phap.FileWatcherEx/FileSystemWatcherEx.cs similarity index 100% rename from Source/FileWatcherEx/FileSystemWatcherEx.cs rename to Source/D2Phap.FileWatcherEx/FileSystemWatcherEx.cs diff --git a/Source/FileWatcherEx/Helpers/EventNormalizer.cs b/Source/D2Phap.FileWatcherEx/Helpers/EventNormalizer.cs similarity index 100% rename from Source/FileWatcherEx/Helpers/EventNormalizer.cs rename to Source/D2Phap.FileWatcherEx/Helpers/EventNormalizer.cs diff --git a/Source/FileWatcherEx/Helpers/EventProcessor.cs b/Source/D2Phap.FileWatcherEx/Helpers/EventProcessor.cs similarity index 100% rename from Source/FileWatcherEx/Helpers/EventProcessor.cs rename to Source/D2Phap.FileWatcherEx/Helpers/EventProcessor.cs diff --git a/Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs b/Source/D2Phap.FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs similarity index 100% rename from Source/FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs rename to Source/D2Phap.FileWatcherEx/Helpers/FileSystemWatcherWrapper.cs diff --git a/Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs b/Source/D2Phap.FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs similarity index 100% rename from Source/FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs rename to Source/D2Phap.FileWatcherEx/Helpers/SymlinkAwareFileWatcher.cs diff --git a/Source/FileWatcherEx/IFileSystemWatcherEx.cs b/Source/D2Phap.FileWatcherEx/IFileSystemWatcherEx.cs similarity index 100% rename from Source/FileWatcherEx/IFileSystemWatcherEx.cs rename to Source/D2Phap.FileWatcherEx/IFileSystemWatcherEx.cs diff --git a/Source/Demo/Demo.WinForms.csproj b/Source/Demo/Demo.WinForms.csproj index 74245b7..100d947 100644 --- a/Source/Demo/Demo.WinForms.csproj +++ b/Source/Demo/Demo.WinForms.csproj @@ -9,7 +9,7 @@ - + \ No newline at end of file diff --git a/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj b/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj index 6dcb059..9e11035 100644 --- a/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj +++ b/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj @@ -8,7 +8,7 @@ - + diff --git a/Source/FileWatcherEx.slnx b/Source/FileWatcherEx.slnx index 1edf624..868af94 100644 --- a/Source/FileWatcherEx.slnx +++ b/Source/FileWatcherEx.slnx @@ -1,6 +1,6 @@ - + diff --git a/Source/FileWatcherExTests/FileWatcherExTests.csproj b/Source/FileWatcherExTests/FileWatcherExTests.csproj index 12b59c3..fa90e61 100644 --- a/Source/FileWatcherExTests/FileWatcherExTests.csproj +++ b/Source/FileWatcherExTests/FileWatcherExTests.csproj @@ -24,7 +24,7 @@ - +