├── .github
├── FUNDING.yml
└── workflows
│ └── dotnet.yml
├── .gitignore
├── LICENSE
├── README.md
├── Source
├── .gitignore
├── Demo
│ ├── Demo.WinForms.csproj
│ ├── Form1.Designer.cs
│ ├── Form1.cs
│ ├── Form1.resx
│ ├── Program.cs
│ └── README.md
├── FileSystemEventRecorder
│ ├── FileSystemEventRecorder.cs
│ ├── FileSystemEventRecorder.csproj
│ └── README.md
├── FileWatcherEx.sln
├── FileWatcherEx
│ ├── FileEvents.cs
│ ├── FileSystemWatcherEx.cs
│ ├── FileWatcherEx.csproj
│ ├── Helpers
│ │ ├── EventNormalizer.cs
│ │ ├── EventProcessor.cs
│ │ ├── FileSystemWatcherWrapper.cs
│ │ └── SymlinkAwareFileWatcher.cs
│ └── IFileSystemWatcherEx.cs
└── FileWatcherExTests
│ ├── EventNormalizerTest.cs
│ ├── FileWatcherExIntegrationTest.cs
│ ├── FileWatcherExTests.csproj
│ ├── Helper
│ └── TempDir.cs
│ ├── ReplayFileSystemWatcherWrapper.cs
│ ├── SymlinkAwareFileWatcherTest.cs
│ └── scenario
│ ├── README.md
│ ├── create_and_remove_file.csv
│ ├── create_and_rename_file_via_explorer.csv
│ ├── create_and_rename_file_wsl2.csv
│ ├── create_file.csv
│ ├── create_file_inside_symbolic_link_directory.csv
│ ├── create_file_wsl2.csv
│ ├── create_rename_and_delete_file_via_explorer.csv
│ ├── create_rename_and_remove_file.csv
│ ├── create_rename_and_remove_file_with_wait_time_wsl2.csv
│ ├── create_rename_and_remove_file_wsl2.csv
│ ├── create_subdirectory_add_and_remove_file.csv
│ ├── create_subdirectory_add_and_remove_file_with_sleep.csv
│ └── download_image_via_Edge_browser.csv
└── nuget.ps1
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [d2phap]
4 | patreon: d2phap
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | custom: ["https://donate.stripe.com/6oE15Kab3740du828a", "https://www.paypal.me/d2phap"]
9 |
--------------------------------------------------------------------------------
/.github/workflows/dotnet.yml:
--------------------------------------------------------------------------------
1 | # This workflow will build a .NET project
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
3 |
4 | name: .NET
5 |
6 | on:
7 | push:
8 | branches: [ "main" ]
9 | pull_request:
10 | branches: [ "main" ]
11 |
12 | defaults:
13 | run:
14 | working-directory: Source
15 |
16 | jobs:
17 | build:
18 |
19 | runs-on: windows-latest
20 |
21 | steps:
22 | - uses: actions/checkout@v3
23 | - name: Setup .NET
24 | uses: actions/setup-dotnet@v3
25 | with:
26 | dotnet-version: 8.0.x
27 | - name: Restore dependencies
28 | run: dotnet restore
29 | - name: Build
30 | run: dotnet build --no-restore
31 | - name: Test
32 | run: dotnet test --no-build --verbosity normal
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | obj
3 | *.suo
4 | *.user
5 | *.orig
6 | *.sdf
7 | *.opensdf
8 | ipch
9 | debug/
10 | release/
11 | TestResults/
12 | packages/
13 | .vs/
14 | BuildResults.xml
15 | *.results.xml
16 | *.log
17 | *.wrn
18 | *.err
19 | intermediate
20 | *.userprefs
21 | !SDB/dependencies/*.dll
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Duong Dieu Phap
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FileWatcherEx for Windows
2 | 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.
3 |
4 | This project is based on the *VSCode FileWatcher*: https://github.com/Microsoft/vscode-filewatcher-windows
5 |
6 | 
7 |
8 |
9 | ## Resource links
10 | - Nuget package: [https://www.nuget.org/packages/FileWatcherEx](https://www.nuget.org/packages/FileWatcherEx/)
11 | - Project url: [https://github.com/d2phap/FileWatcherEx](https://github.com/d2phap/FileWatcherEx)
12 | - Website: [https://imageglass.org](https://imageglass.org)
13 |
14 | ## Features
15 | - Standardizes the events of `System.IO.FileSystemWatcher`.
16 | - No false change notifications when a file system item is created, deleted, changed or renamed.
17 | - Supports .NET 6.0, 7.0.
18 |
19 | ## Installation
20 | Run the command:
21 |
22 | ```bash
23 | # Nuget package
24 | Install-Package FileWatcherEx
25 | ```
26 |
27 | ## Usage
28 | See Demo project for full details!
29 |
30 | ```cs
31 | using FileWatcherEx;
32 |
33 |
34 | var _fw = new FileSystemWatcherEx(@"C:\path\to\watch");
35 |
36 | // event handlers
37 | _fw.OnRenamed += FW_OnRenamed;
38 | _fw.OnCreated += FW_OnCreated;
39 | _fw.OnDeleted += FW_OnDeleted;
40 | _fw.OnChanged += FW_OnChanged;
41 | _fw.OnError += FW_OnError;
42 |
43 | // thread-safe for event handlers
44 | _fw.SynchronizingObject = this;
45 |
46 | // start watching
47 | _fw.Start();
48 |
49 |
50 |
51 | void FW_OnRenamed(object sender, FileChangedEvent e)
52 | {
53 | // do something here
54 | }
55 | ...
56 |
57 | ```
58 |
59 | ## License
60 | [MIT](LICENSE)
61 |
62 | ## Support this project
63 | - [GitHub sponsor](https://github.com/sponsors/d2phap)
64 | - [Patreon](https://www.patreon.com/d2phap)
65 | - [PayPal](https://www.paypal.me/d2phap)
66 | - [Wire Transfers](https://donorbox.org/imageglass)
67 |
68 | Thanks for your gratitude and finance help!
69 |
--------------------------------------------------------------------------------
/Source/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 |
--------------------------------------------------------------------------------
/Source/Demo/Demo.WinForms.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | net8.0-windows
6 | enable
7 | true
8 | enable
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/Source/Demo/Form1.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace Demo
2 | {
3 | partial class Form1
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 |
24 | #region Windows Form Designer generated code
25 |
26 | ///
27 | /// Required method for Designer support - do not modify
28 | /// the contents of this method with the code editor.
29 | ///
30 | private void InitializeComponent()
31 | {
32 | this.txtConsole = new System.Windows.Forms.TextBox();
33 | this.btnSelectFolder = new System.Windows.Forms.Button();
34 | this.txtPath = new System.Windows.Forms.TextBox();
35 | this.btnStart = new System.Windows.Forms.Button();
36 | this.btnStop = new System.Windows.Forms.Button();
37 | this.SuspendLayout();
38 | //
39 | // txtConsole
40 | //
41 | this.txtConsole.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
42 | | System.Windows.Forms.AnchorStyles.Left)
43 | | System.Windows.Forms.AnchorStyles.Right)));
44 | this.txtConsole.BackColor = System.Drawing.Color.Black;
45 | this.txtConsole.BorderStyle = System.Windows.Forms.BorderStyle.None;
46 | this.txtConsole.Font = new System.Drawing.Font("Segoe UI", 10F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
47 | this.txtConsole.ForeColor = System.Drawing.Color.White;
48 | this.txtConsole.Location = new System.Drawing.Point(0, 0);
49 | this.txtConsole.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
50 | this.txtConsole.Multiline = true;
51 | this.txtConsole.Name = "txtConsole";
52 | this.txtConsole.ReadOnly = true;
53 | this.txtConsole.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
54 | this.txtConsole.Size = new System.Drawing.Size(1174, 546);
55 | this.txtConsole.TabIndex = 0;
56 | //
57 | // btnSelectFolder
58 | //
59 | this.btnSelectFolder.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
60 | this.btnSelectFolder.Location = new System.Drawing.Point(526, 574);
61 | this.btnSelectFolder.Name = "btnSelectFolder";
62 | this.btnSelectFolder.Size = new System.Drawing.Size(133, 57);
63 | this.btnSelectFolder.TabIndex = 1;
64 | this.btnSelectFolder.Text = "Select folder";
65 | this.btnSelectFolder.UseVisualStyleBackColor = true;
66 | this.btnSelectFolder.Click += new System.EventHandler(this.BtnSelectFolder_Click);
67 | //
68 | // txtPath
69 | //
70 | this.txtPath.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left)));
71 | this.txtPath.Enabled = false;
72 | this.txtPath.Location = new System.Drawing.Point(12, 574);
73 | this.txtPath.Multiline = true;
74 | this.txtPath.Name = "txtPath";
75 | this.txtPath.Size = new System.Drawing.Size(508, 57);
76 | this.txtPath.TabIndex = 2;
77 | this.txtPath.Text = "C:\\";
78 | //
79 | // btnStart
80 | //
81 | this.btnStart.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
82 | this.btnStart.Location = new System.Drawing.Point(944, 574);
83 | this.btnStart.Name = "btnStart";
84 | this.btnStart.Size = new System.Drawing.Size(104, 57);
85 | this.btnStart.TabIndex = 3;
86 | this.btnStart.Text = "Start";
87 | this.btnStart.UseVisualStyleBackColor = true;
88 | this.btnStart.Click += new System.EventHandler(this.BtnStart_Click);
89 | //
90 | // btnStop
91 | //
92 | this.btnStop.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
93 | this.btnStop.Enabled = false;
94 | this.btnStop.Location = new System.Drawing.Point(1054, 574);
95 | this.btnStop.Name = "btnStop";
96 | this.btnStop.Size = new System.Drawing.Size(104, 57);
97 | this.btnStop.TabIndex = 4;
98 | this.btnStop.Text = "Stop";
99 | this.btnStop.UseVisualStyleBackColor = true;
100 | this.btnStop.Click += new System.EventHandler(this.BtnStop_Click);
101 | //
102 | // Form1
103 | //
104 | this.AutoScaleDimensions = new System.Drawing.SizeF(9F, 23F);
105 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
106 | this.ClientSize = new System.Drawing.Size(1174, 654);
107 | this.Controls.Add(this.btnStop);
108 | this.Controls.Add(this.btnStart);
109 | this.Controls.Add(this.txtPath);
110 | this.Controls.Add(this.btnSelectFolder);
111 | this.Controls.Add(this.txtConsole);
112 | this.Font = new System.Drawing.Font("Segoe UI", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
113 | this.Margin = new System.Windows.Forms.Padding(3, 4, 3, 4);
114 | this.Name = "Form1";
115 | this.Text = "Demo.WinForms";
116 | this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
117 | this.ResumeLayout(false);
118 | this.PerformLayout();
119 |
120 | }
121 |
122 | #endregion
123 |
124 | private System.Windows.Forms.TextBox txtConsole;
125 | private System.Windows.Forms.Button btnSelectFolder;
126 | private System.Windows.Forms.TextBox txtPath;
127 | private System.Windows.Forms.Button btnStart;
128 | private System.Windows.Forms.Button btnStop;
129 | }
130 | }
--------------------------------------------------------------------------------
/Source/Demo/Form1.cs:
--------------------------------------------------------------------------------
1 |
2 | using FileWatcherEx;
3 |
4 | namespace Demo
5 | {
6 | public partial class Form1 : Form
7 | {
8 | private FileSystemWatcherEx _fw = new();
9 |
10 | public Form1()
11 | {
12 | InitializeComponent();
13 | }
14 |
15 |
16 |
17 | private void BtnStart_Click(object sender, EventArgs e)
18 | {
19 | _fw = new FileSystemWatcherEx(txtPath.Text.Trim(), FW_OnLog);
20 |
21 | _fw.OnRenamed += FW_OnRenamed;
22 | _fw.OnCreated += FW_OnCreated;
23 | _fw.OnDeleted += FW_OnDeleted;
24 | _fw.OnChanged += FW_OnChanged;
25 | _fw.OnError += FW_OnError;
26 |
27 | _fw.SynchronizingObject = this;
28 | _fw.IncludeSubdirectories = true;
29 |
30 | try
31 | {
32 | _fw.Start();
33 |
34 | btnStart.Enabled = true;
35 | btnSelectFolder.Enabled = false;
36 | txtPath.Enabled = false;
37 | btnStop.Enabled = true;
38 | }
39 | catch (Exception ex)
40 | {
41 | MessageBox.Show(ex.Message);
42 | }
43 | }
44 |
45 | private void FW_OnError(object? sender, ErrorEventArgs e)
46 | {
47 | if (txtConsole.InvokeRequired)
48 | {
49 | txtConsole.Invoke(FW_OnError, sender, e);
50 | }
51 | else
52 | {
53 | txtConsole.Text += "[ERROR]: " + e.GetException().Message + "\r\n";
54 | }
55 | }
56 |
57 | private void FW_OnChanged(object? sender, FileChangedEvent e)
58 | {
59 | txtConsole.Text += string.Format("[cha] {0} | {1}",
60 | Enum.GetName(typeof(ChangeType), e.ChangeType),
61 | e.FullPath) + "\r\n";
62 | }
63 |
64 | private void FW_OnDeleted(object? sender, FileChangedEvent e)
65 | {
66 | txtConsole.Text += string.Format("[del] {0} | {1}",
67 | Enum.GetName(typeof(ChangeType), e.ChangeType),
68 | e.FullPath) + "\r\n";
69 | }
70 |
71 | private void FW_OnCreated(object? sender, FileChangedEvent e)
72 | {
73 | txtConsole.Text += string.Format("[cre] {0} | {1}",
74 | Enum.GetName(typeof(ChangeType), e.ChangeType),
75 | e.FullPath) + "\r\n";
76 | }
77 |
78 | private void FW_OnRenamed(object? sender, FileChangedEvent e)
79 | {
80 | txtConsole.Text += string.Format("[ren] {0} | {1} ----> {2}",
81 | Enum.GetName(typeof(ChangeType), e.ChangeType),
82 | e.OldFullPath,
83 | e.FullPath) + "\r\n";
84 | }
85 |
86 | private void FW_OnLog(string value)
87 | {
88 | txtConsole.Text += $@"[log] {value}" + "\r\n";
89 | }
90 |
91 | private void BtnStop_Click(object sender, EventArgs e)
92 | {
93 | _fw.Stop();
94 |
95 | btnStart.Enabled = true;
96 | btnSelectFolder.Enabled = true;
97 | txtPath.Enabled = true;
98 | btnStop.Enabled = false;
99 | btnStop.Enabled = true;
100 | }
101 |
102 |
103 | private void BtnSelectFolder_Click(object sender, EventArgs e)
104 | {
105 | var fb = new FolderBrowserDialog();
106 |
107 |
108 | if (fb.ShowDialog() == DialogResult.OK)
109 | {
110 | txtPath.Text = fb.SelectedPath;
111 |
112 | _fw.Stop();
113 | _fw.Dispose();
114 | }
115 | }
116 |
117 | private void Form1_FormClosing(object sender, FormClosingEventArgs e)
118 | {
119 | _fw.Dispose();
120 | }
121 | }
122 | }
--------------------------------------------------------------------------------
/Source/Demo/Form1.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | text/microsoft-resx
50 |
51 |
52 | 2.0
53 |
54 |
55 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
56 |
57 |
58 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
59 |
60 |
--------------------------------------------------------------------------------
/Source/Demo/Program.cs:
--------------------------------------------------------------------------------
1 | namespace Demo
2 | {
3 | internal static class Program
4 | {
5 | ///
6 | /// The main entry point for the application.
7 | ///
8 | [STAThread]
9 | static void Main()
10 | {
11 | ApplicationConfiguration.Initialize();
12 | Application.Run(new Form1());
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/Source/Demo/README.md:
--------------------------------------------------------------------------------
1 | Manual Testing
2 | --------------
3 |
4 | This little application is helpful for interactive testing of the library.
5 |
6 | Symlinks
7 | --------
8 | To create a symlink directory on Windows, use `mklink`.
9 | Example: `mklink /D my-symbolic-link c:\temp\target-directory`
10 |
--------------------------------------------------------------------------------
/Source/FileSystemEventRecorder/FileSystemEventRecorder.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 | using System.Diagnostics;
3 | using System.Globalization;
4 | using CsvHelper;
5 |
6 | namespace FileSystemEventRecorder;
7 |
8 | // event received from C# FileSystemWatcher
9 | internal record EventRecord(
10 | string FullPath,
11 | string EventName,
12 | string? OldFullPath, // only provided by "rename" event
13 | long NowInTicks
14 | );
15 |
16 | // post processed. Calculated before closing program.
17 | // data is written to CSV already in the format FileSystemEventArgs requires it (separate dir + filename)
18 | internal record EventRecordWithDiff(
19 | string Directory,
20 | string FileName,
21 | string EventName,
22 | string? OldFileName,
23 | long DiffInTicks, // ticks between passed by from the previous event to now
24 | double DiffInMilliseconds // milliseconds between previous event and now.
25 | );
26 |
27 | ///
28 | /// Command line tool to capture the raw events of the native FileSystemWatcher class in a CSV file
29 | ///
30 | public static class FileSystemEventRecords
31 | {
32 | private static readonly ConcurrentQueue EventRecords = new();
33 |
34 | public static void Main(string[] args)
35 | {
36 | var (watchedDirectory, csvOutputFile) = ProcessArguments(args);
37 |
38 | var watcher = new FileSystemWatcher();
39 | watcher.Path = watchedDirectory;
40 | watcher.IncludeSubdirectories = true;
41 | watcher.NotifyFilter = NotifyFilters.LastWrite
42 | | NotifyFilters.FileName
43 | | NotifyFilters.DirectoryName;
44 |
45 | watcher.Created += (_, ev) =>
46 | EventRecords.Enqueue(new EventRecord(ev.FullPath, "created", null, Stopwatch.GetTimestamp()));
47 | watcher.Deleted += (_, ev) =>
48 | EventRecords.Enqueue(new EventRecord(ev.FullPath, "deleted", null, Stopwatch.GetTimestamp()));
49 |
50 | watcher.Changed += (_, ev) =>
51 | EventRecords.Enqueue(new EventRecord(ev.FullPath, "changed", null, Stopwatch.GetTimestamp()));
52 | watcher.Renamed += (_, ev) =>
53 | EventRecords.Enqueue(new EventRecord(ev.FullPath, "renamed", ev.OldFullPath, Stopwatch.GetTimestamp()));
54 | watcher.Error += (_, ev) =>
55 | {
56 | EventRecords.Enqueue(new EventRecord("", "error", null, Stopwatch.GetTimestamp()));
57 | Console.WriteLine($"Error: {ev.GetException()}");
58 | };
59 |
60 | // taken from existing code
61 | watcher.InternalBufferSize = 32768;
62 | watcher.EnableRaisingEvents = true;
63 |
64 | Console.WriteLine($"Recording. Now go ahead and perform the desired file system activities in {watchedDirectory}. " +
65 | "Press CTRL + C to stop the recording.");
66 | Console.CancelKeyPress += (_, _) =>
67 | {
68 | Console.WriteLine("Exiting.");
69 | ProcessQueueAndWriteToDisk(csvOutputFile);
70 | Environment.Exit(0);
71 | };
72 |
73 | while (true)
74 | {
75 | Thread.Sleep(200);
76 | }
77 | }
78 |
79 | private static (string, string) ProcessArguments(string[] args)
80 | {
81 | if (args.Length < 2)
82 | {
83 | Console.WriteLine("Usage: dotnet run [directory to be watched] [output csv file]");
84 | Environment.Exit(1);
85 | }
86 |
87 | return (args[0], args[1]);
88 | }
89 |
90 | private static void ProcessQueueAndWriteToDisk(string csvOutputFile)
91 | {
92 | if (EventRecords.IsEmpty)
93 | {
94 | Console.WriteLine("Detected no file system events. Nothing is written.");
95 | }
96 | else
97 | {
98 | Console.WriteLine($"Recorded {EventRecords.Count} file system events.");
99 | var records = MapToDiffTicks();
100 |
101 | Console.WriteLine($"Writing CSV to {csvOutputFile}.");
102 | using (var writer = new StreamWriter(csvOutputFile))
103 | using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
104 | {
105 | csv.WriteRecords(records);
106 | }
107 |
108 | Console.WriteLine("Done.");
109 | }
110 | }
111 |
112 | // post-process queue. Calculate difference between previous and current event
113 | private static IEnumerable MapToDiffTicks()
114 | {
115 | List eventsWithDiffs = new();
116 | long previousTicks = 0;
117 | foreach (var eventRecord in EventRecords)
118 | {
119 | var diff = previousTicks switch
120 | {
121 | 0 => 0, // first run
122 | _ => eventRecord.NowInTicks - previousTicks
123 | };
124 |
125 | previousTicks = eventRecord.NowInTicks;
126 | double diffInMilliseconds = Convert.ToInt64(new TimeSpan(diff).TotalMilliseconds);
127 |
128 | var directory = Path.GetDirectoryName(eventRecord.FullPath) ?? "";
129 | var fileName = Path.GetFileName(eventRecord.FullPath);
130 | var oldFileName = Path.GetFileName(eventRecord.OldFullPath);
131 |
132 | var record = new EventRecordWithDiff(
133 | directory,
134 | fileName,
135 | eventRecord.EventName,
136 | oldFileName,
137 | diff,
138 | diffInMilliseconds);
139 | eventsWithDiffs.Add(record);
140 | }
141 |
142 | return eventsWithDiffs;
143 | }
144 | }
--------------------------------------------------------------------------------
/Source/FileSystemEventRecorder/FileSystemEventRecorder.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net8.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Source/FileSystemEventRecorder/README.md:
--------------------------------------------------------------------------------
1 | # File System Event Recorder
2 |
3 | Command line tool to capture the raw events of [FileSystemWatcher](https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher)
4 | in a CSV file. The CSV file can than be used to write integration tests against *FileWatcherEx*.
5 |
6 | Usage:
7 | ````sh
8 | dotnet run C:\temp\fwtest\ C:\temp\fwevents.csv
9 | ````
10 |
11 | Example output:
12 | ````csv
13 | Directory,FileName,EventName,OldFileName,DiffInTicks,DiffInMilliseconds
14 | C:\temp\fwtest,a.txt,created,,0,0
15 | C:\temp\fwtest,b.txt,renamed,a.txt,1265338,127
16 | C:\temp\fwtest,b.txt,deleted,,6660690,666
17 | ````
--------------------------------------------------------------------------------
/Source/FileWatcherEx.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.1.31911.260
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileWatcherEx", "FileWatcherEx\FileWatcherEx.csproj", "{CF81727D-B6EC-4202-8E78-087C5D5EABF3}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Demo.WinForms", "Demo\Demo.WinForms.csproj", "{F973F462-7769-433C-AEC1-17AF2902ED2D}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileWatcherExTests", "FileWatcherExTests\FileWatcherExTests.csproj", "{1C0CA67C-369E-4258-B661-2C545B50A6FF}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSystemEventRecorder", "FileSystemEventRecorder\FileSystemEventRecorder.csproj", "{F87993D7-2487-41BD-9044-EBEB54BAD13C}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {CF81727D-B6EC-4202-8E78-087C5D5EABF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {CF81727D-B6EC-4202-8E78-087C5D5EABF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {CF81727D-B6EC-4202-8E78-087C5D5EABF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {CF81727D-B6EC-4202-8E78-087C5D5EABF3}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {F973F462-7769-433C-AEC1-17AF2902ED2D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {F973F462-7769-433C-AEC1-17AF2902ED2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {F973F462-7769-433C-AEC1-17AF2902ED2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {F973F462-7769-433C-AEC1-17AF2902ED2D}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {1C0CA67C-369E-4258-B661-2C545B50A6FF}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {F87993D7-2487-41BD-9044-EBEB54BAD13C}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {7A615E75-F056-422F-952E-9C0CD34E477B}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/Source/FileWatcherEx/FileEvents.cs:
--------------------------------------------------------------------------------
1 | namespace FileWatcherEx;
2 |
3 | public enum ChangeType
4 | {
5 | CHANGED = 0,
6 | CREATED = 1,
7 | DELETED = 2,
8 | RENAMED = 3,
9 | LOG = 4,
10 | }
11 |
12 |
13 | public class FileChangedEvent
14 | {
15 | ///
16 | /// Type of change event
17 | ///
18 | public ChangeType ChangeType { get; set; }
19 |
20 | ///
21 | /// The full path
22 | ///
23 | public string FullPath { get; set; } = "";
24 |
25 | ///
26 | /// The old full path (used if ChangeType = RENAMED)
27 | ///
28 | public string? OldFullPath { get; set; } = "";
29 | }
30 |
31 |
32 |
--------------------------------------------------------------------------------
/Source/FileWatcherEx/FileSystemWatcherEx.cs:
--------------------------------------------------------------------------------
1 |
2 | using System.Collections.Concurrent;
3 | using System.ComponentModel;
4 | using FileWatcherEx.Helpers;
5 |
6 | namespace FileWatcherEx;
7 |
8 | ///
9 | /// A wrapper of to standardize the events
10 | /// and avoid false change notifications.
11 | ///
12 | public class FileSystemWatcherEx : IDisposable, IFileSystemWatcherEx
13 | {
14 | #region Private Properties
15 |
16 | private Thread? _thread;
17 | private EventProcessor? _processor;
18 | private readonly BlockingCollection _fileEventQueue = new();
19 |
20 | private SymlinkAwareFileWatcher? _watcher;
21 | private Func? _fswFactory;
22 | private readonly Action _logger;
23 |
24 | // Define the cancellation token.
25 | private CancellationTokenSource? _cancelSource;
26 |
27 | // allow injection of FileSystemWatcherWrapper
28 | internal Func FileSystemWatcherFactory
29 | {
30 | // default to production FileSystemWatcherWrapper (which wrapped the native FileSystemWatcher)
31 | get { return _fswFactory ?? (() => new FileSystemWatcherWrapper()); }
32 | set => _fswFactory = value;
33 | }
34 |
35 | #endregion
36 |
37 |
38 | #region Public Properties
39 |
40 | ///
41 | /// Gets or sets the path of the directory to watch.
42 | ///
43 | public string FolderPath { get; set; } = "";
44 |
45 |
46 | ///
47 | /// Gets the collection of all the filters used to determine what files are monitored in a directory.
48 | ///
49 | public System.Collections.ObjectModel.Collection Filters { get; } = new();
50 |
51 |
52 | ///
53 | /// Gets or sets the filter string used to determine what files are monitored in a directory.
54 | ///
55 | public string Filter
56 | {
57 | get => Filters.Count == 0 ? "*" : Filters[0];
58 | set
59 | {
60 | Filters.Clear();
61 | Filters.Add(value);
62 | }
63 | }
64 |
65 |
66 | ///
67 | /// Gets or sets the type of changes to watch for.
68 | /// The default is the bitwise OR combination of
69 | /// ,
70 | /// ,
71 | /// and .
72 | ///
73 | public NotifyFilters NotifyFilter { get; set; } = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
74 |
75 |
76 | ///
77 | /// Gets or sets a value indicating whether subdirectories within the specified path should be monitored.
78 | ///
79 | public bool IncludeSubdirectories { get; set; } = false;
80 |
81 |
82 | ///
83 | /// Gets or sets the object used to marshal the event handler calls issued as a result of a directory change.
84 | ///
85 | public ISynchronizeInvoke? SynchronizingObject { get; set; }
86 |
87 | #endregion
88 |
89 |
90 | #region Public Events
91 |
92 | ///
93 | /// Occurs when a file or directory in the specified
94 | /// is changed.
95 | ///
96 | public event DelegateOnChanged? OnChanged;
97 | public delegate void DelegateOnChanged(object? sender, FileChangedEvent e);
98 |
99 |
100 | ///
101 | /// Occurs when a file or directory in the specified
102 | /// is deleted.
103 | ///
104 | public event DelegateOnDeleted? OnDeleted;
105 | public delegate void DelegateOnDeleted(object? sender, FileChangedEvent e);
106 |
107 |
108 | ///
109 | /// Occurs when a file or directory in the specified
110 | /// is created.
111 | ///
112 | public event DelegateOnCreated? OnCreated;
113 | public delegate void DelegateOnCreated(object? sender, FileChangedEvent e);
114 |
115 |
116 | ///
117 | /// Occurs when a file or directory in the specified
118 | /// is renamed.
119 | ///
120 | public event DelegateOnRenamed? OnRenamed;
121 | public delegate void DelegateOnRenamed(object? sender, FileChangedEvent e);
122 |
123 |
124 | ///
125 | /// Occurs when the instance of is unable to continue
126 | /// monitoring changes or when the internal buffer overflows.
127 | ///
128 | public event DelegateOnError? OnError;
129 | public delegate void DelegateOnError(object? sender, ErrorEventArgs e);
130 |
131 | #endregion
132 |
133 |
134 | ///
135 | /// Initialize new instance of
136 | ///
137 | ///
138 | /// Optional Action to log out library internals
139 | public FileSystemWatcherEx(string folderPath = "", Action? logger = null)
140 | {
141 | FolderPath = folderPath;
142 | _logger = logger ?? (_ => {}) ;
143 | }
144 |
145 |
146 | ///
147 | /// Start watching files
148 | ///
149 | public void Start()
150 | {
151 | if (!Directory.Exists(FolderPath)) return;
152 | Stop();
153 |
154 |
155 | _processor = new EventProcessor((e) =>
156 | {
157 | switch (e.ChangeType)
158 | {
159 | case ChangeType.CHANGED:
160 |
161 | InvokeChangedEvent(SynchronizingObject, e);
162 |
163 | void InvokeChangedEvent(object? sender, FileChangedEvent fileEvent)
164 | {
165 | if (SynchronizingObject != null && SynchronizingObject.InvokeRequired)
166 | {
167 | SynchronizingObject.Invoke(new Action