diff --git a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs index 16bdbb1074ce5..e3135500d8ce7 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs @@ -408,8 +408,11 @@ private bool MatchPattern(ReadOnlySpan relativePath) /// private void NotifyInternalBufferOverflowEvent() { - _onErrorHandler?.Invoke(this, new ErrorEventArgs( - new InternalBufferOverflowException(SR.Format(SR.FSW_BufferOverflow, _directory)))); + if (_onErrorHandler != null) + { + OnError(new ErrorEventArgs( + new InternalBufferOverflowException(SR.Format(SR.FSW_BufferOverflow, _directory)))); + } } /// @@ -418,11 +421,10 @@ private void NotifyInternalBufferOverflowEvent() private void NotifyRenameEventArgs(WatcherChangeTypes action, ReadOnlySpan name, ReadOnlySpan oldName) { // filter if there's no handler or neither new name or old name match a specified pattern - RenamedEventHandler? handler = _onRenamedHandler; - if (handler != null && + if (_onRenamedHandler != null && (MatchPattern(name) || MatchPattern(oldName))) { - handler(this, new RenamedEventArgs(action, _directory, name.IsEmpty ? null : name.ToString(), oldName.IsEmpty ? null : oldName.ToString())); + OnRenamed(new RenamedEventArgs(action, _directory, name.IsEmpty ? null : name.ToString(), oldName.IsEmpty ? null : oldName.ToString())); } } @@ -451,7 +453,7 @@ private void NotifyFileSystemEventArgs(WatcherChangeTypes changeType, ReadOnlySp if (handler != null && MatchPattern(name.IsEmpty ? _directory : name)) { - handler(this, new FileSystemEventArgs(changeType, _directory, name.IsEmpty ? null : name.ToString())); + InvokeOn(new FileSystemEventArgs(changeType, _directory, name.IsEmpty ? null : name.ToString()), handler); } } @@ -464,7 +466,7 @@ private void NotifyFileSystemEventArgs(WatcherChangeTypes changeType, string nam if (handler != null && MatchPattern(string.IsNullOrEmpty(name) ? _directory : name)) { - handler(this, new FileSystemEventArgs(changeType, _directory, name)); + InvokeOn(new FileSystemEventArgs(changeType, _directory, name), handler); } } diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Changed.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Changed.cs index 69ba87b85dc68..6f40b8f4d364f 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Changed.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Changed.cs @@ -78,5 +78,22 @@ public void FileSystemWatcher_Directory_Changed_SymLink() ExpectEvent(watcher, 0, action, cleanup, dir.Path); } } + + [Fact] + public void FileSystemWatcher_Directory_Changed_SynchronizingObject() + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var dir = new TempDirectory(Path.Combine(testDirectory.Path, "dir"))) + using (var watcher = new FileSystemWatcher(testDirectory.Path, Path.GetFileName(dir.Path))) + { + TestISynchronizeInvoke invoker = new TestISynchronizeInvoke(); + watcher.SynchronizingObject = invoker; + + Action action = () => Directory.SetLastWriteTime(dir.Path, DateTime.Now + TimeSpan.FromSeconds(10)); + + ExpectEvent(watcher, WatcherChangeTypes.Changed, action, expectedPath: dir.Path); + Assert.True(invoker.BeginInvoke_Called); + } + } } } diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Create.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Create.cs index 3cdebbcb6e9d1..c375f3f038c32 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Create.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Create.cs @@ -102,5 +102,25 @@ public void FileSystemWatcher_Directory_Create_SymLink() ExpectEvent(watcher, WatcherChangeTypes.Created, action, cleanup, symLinkPath); } } + + [Fact] + public void FileSystemWatcher_Directory_Create_SynchronizingObject() + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var watcher = new FileSystemWatcher(testDirectory.Path)) + { + TestISynchronizeInvoke invoker = new TestISynchronizeInvoke(); + watcher.SynchronizingObject = invoker; + + string dirName = Path.Combine(testDirectory.Path, "dir"); + watcher.Filter = Path.GetFileName(dirName); + + Action action = () => Directory.CreateDirectory(dirName); + Action cleanup = () => Directory.Delete(dirName); + + ExpectEvent(watcher, WatcherChangeTypes.Created, action, cleanup, dirName); + Assert.True(invoker.BeginInvoke_Called); + } + } } } diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Delete.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Delete.cs index 554d513507ea3..5e5f5ed4a0e64 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Delete.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Delete.cs @@ -85,5 +85,26 @@ public void FileSystemWatcher_Directory_Delete_SymLink() ExpectEvent(watcher, WatcherChangeTypes.Deleted, action, cleanup, expectedPath: symLinkPath); } } + + [Fact] + public void FileSystemWatcher_Directory_Delete_SynchronizingObject() + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var watcher = new FileSystemWatcher(testDirectory.Path)) + { + TestISynchronizeInvoke invoker = new TestISynchronizeInvoke(); + watcher.SynchronizingObject = invoker; + + string dirName = Path.Combine(testDirectory.Path, "dir"); + watcher.Filter = Path.GetFileName(dirName); + + Action action = () => Directory.Delete(dirName); + Action cleanup = () => Directory.CreateDirectory(dirName); + cleanup(); + + ExpectEvent(watcher, WatcherChangeTypes.Deleted, action, cleanup, dirName); + Assert.True(invoker.BeginInvoke_Called); + } + } } } diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Move.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Move.cs index 63e854dd0c008..fac97f1ce0010 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Move.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Move.cs @@ -93,6 +93,27 @@ public void Directory_Move_With_Set_NotifyFilter() DirectoryMove_WithNotifyFilter(WatcherChangeTypes.Renamed); } + [Fact] + public void Directory_Move_SynchronizingObject() + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var dir = new TempDirectory(Path.Combine(testDirectory.Path, "dir"))) + using (var watcher = new FileSystemWatcher(testDirectory.Path)) + { + TestISynchronizeInvoke invoker = new TestISynchronizeInvoke(); + watcher.SynchronizingObject = invoker; + + string sourcePath = dir.Path; + string targetPath = Path.Combine(testDirectory.Path, "target"); + + Action action = () => Directory.Move(sourcePath, targetPath); + Action cleanup = () => Directory.Move(targetPath, sourcePath); + + ExpectEvent(watcher, WatcherChangeTypes.Renamed, action, cleanup, targetPath); + Assert.True(invoker.BeginInvoke_Called); + } + } + #region Test Helpers private void DirectoryMove_SameDirectory(WatcherChangeTypes eventType) diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Changed.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Changed.cs index 4f0bb1a60a7c3..da7b508c836da 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Changed.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Changed.cs @@ -82,5 +82,22 @@ public void FileSystemWatcher_File_Changed_SymLink() ExpectEvent(watcher, 0, action, cleanup, expectedPath: file.Path); } } + + [Fact] + public void FileSystemWatcher_File_Changed_SynchronizingObject() + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var file = new TempFile(Path.Combine(testDirectory.Path, "file"))) + using (var watcher = new FileSystemWatcher(testDirectory.Path, Path.GetFileName(file.Path))) + { + TestISynchronizeInvoke invoker = new TestISynchronizeInvoke(); + watcher.SynchronizingObject = invoker; + + Action action = () => Directory.SetLastWriteTime(file.Path, DateTime.Now + TimeSpan.FromSeconds(10)); + + ExpectEvent(watcher, WatcherChangeTypes.Changed, action, expectedPath: file.Path); + Assert.True(invoker.BeginInvoke_Called); + } + } } } diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Create.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Create.cs index 7603e5aea9d62..3a190a6899895 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Create.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Create.cs @@ -141,5 +141,25 @@ public void FileSystemWatcher_File_Create_SymLink() ExpectEvent(watcher, WatcherChangeTypes.Created, action, cleanup, symLinkPath); } } + + [Fact] + public void FileSystemWatcher_File_Create_SynchronizingObject() + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var watcher = new FileSystemWatcher(testDirectory.Path)) + { + TestISynchronizeInvoke invoker = new TestISynchronizeInvoke(); + watcher.SynchronizingObject = invoker; + + string fileName = Path.Combine(testDirectory.Path, "file"); + watcher.Filter = Path.GetFileName(fileName); + + Action action = () => File.Create(fileName).Dispose(); + Action cleanup = () => File.Delete(fileName); + + ExpectEvent(watcher, WatcherChangeTypes.Created, action, cleanup, fileName); + Assert.True(invoker.BeginInvoke_Called); + } + } } } diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Delete.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Delete.cs index 3e93db64bd429..bbb0a95069d8a 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Delete.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Delete.cs @@ -106,5 +106,26 @@ public void FileSystemWatcher_File_Delete_SymLink() ExpectEvent(watcher, WatcherChangeTypes.Deleted, action, cleanup, symLinkPath); } } + + [Fact] + public void FileSystemWatcher_File_Delete_SynchronizingObject() + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var watcher = new FileSystemWatcher(testDirectory.Path)) + { + TestISynchronizeInvoke invoker = new TestISynchronizeInvoke(); + watcher.SynchronizingObject = invoker; + + string fileName = Path.Combine(testDirectory.Path, "file"); + watcher.Filter = Path.GetFileName(fileName); + + Action action = () => File.Delete(fileName); + Action cleanup = () => File.Create(fileName).Dispose(); + cleanup(); + + ExpectEvent(watcher, WatcherChangeTypes.Deleted, action, cleanup, fileName); + Assert.True(invoker.BeginInvoke_Called); + } + } } } diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Move.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Move.cs index c9a36a577962f..97cf4242d3381 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Move.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Move.cs @@ -121,6 +121,29 @@ public void Unix_File_Move_With_Set_NotifyFilter() FileMove_WithNotifyFilter(WatcherChangeTypes.Renamed); } + [Fact] + public void File_Move_SynchronizingObject() + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var dir = new TempDirectory(Path.Combine(testDirectory.Path, "dir"))) + using (var testFile = new TempFile(Path.Combine(dir.Path, "file"))) + using (var watcher = new FileSystemWatcher(dir.Path, "*")) + { + TestISynchronizeInvoke invoker = new TestISynchronizeInvoke(); + watcher.SynchronizingObject = invoker; + + string sourcePath = testFile.Path; + string targetPath = testFile.Path + "_Renamed"; + + // Move the testFile to a different name in the same directory + Action action = () => File.Move(sourcePath, targetPath); + Action cleanup = () => File.Move(targetPath, sourcePath); + + ExpectEvent(watcher, WatcherChangeTypes.Renamed, action, cleanup, targetPath); + Assert.True(invoker.BeginInvoke_Called); + } + } + #region Test Helpers private void FileMove_SameDirectory(WatcherChangeTypes eventType) diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.InternalBufferSize.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.InternalBufferSize.cs index 5d76ba1923fc8..309f1ba98e8c4 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.InternalBufferSize.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.InternalBufferSize.cs @@ -44,34 +44,19 @@ public class InternalBufferSizeTests : FileSystemWatcherTest [PlatformSpecific(TestPlatforms.Windows)] // Uses P/Invokes public void FileSystemWatcher_InternalBufferSize(bool setToHigherCapacity) { + ManualResetEvent unblockHandler = new ManualResetEvent(false); using (var testDirectory = new TempDirectory(GetTestFilePath())) using (var file = new TempFile(Path.Combine(testDirectory.Path, "file"))) - using (var watcher = new FileSystemWatcher(testDirectory.Path, Path.GetFileName(file.Path))) + using (FileSystemWatcher watcher = CreateWatcher(testDirectory.Path, file.Path, unblockHandler)) { - int internalBufferOperationCapacity = watcher.InternalBufferSize / (17 + Path.GetFileName(file.Path).Length); + int internalBufferOperationCapacity = CalculateInternalBufferOperationCapacity(watcher.InternalBufferSize, file.Path); // Set the capacity high to ensure no error events arise. if (setToHigherCapacity) watcher.InternalBufferSize = watcher.InternalBufferSize * 12; - // block the handling thread - ManualResetEvent unblockHandler = new ManualResetEvent(false); - watcher.Changed += (o, e) => - { - unblockHandler.WaitOne(); - }; - - // generate enough file change events to overflow the default buffer - Action action = () => - { - for (int i = 1; i < internalBufferOperationCapacity * 10; i++) - { - File.SetLastWriteTime(file.Path, DateTime.Now + TimeSpan.FromSeconds(i)); - } - - unblockHandler.Set(); - }; - Action cleanup = () => unblockHandler.Reset(); + Action action = GetAction(unblockHandler, internalBufferOperationCapacity, file.Path); + Action cleanup = GetCleanup(unblockHandler); if (setToHigherCapacity) ExpectNoError(watcher, action, cleanup); @@ -79,5 +64,60 @@ public void FileSystemWatcher_InternalBufferSize(bool setToHigherCapacity) ExpectError(watcher, action, cleanup); } } + + [Fact] + [PlatformSpecific(TestPlatforms.Windows)] + public void FileSystemWatcher_InternalBufferSize_SynchronizingObject() + { + ManualResetEvent unblockHandler = new ManualResetEvent(false); + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var file = new TempFile(Path.Combine(testDirectory.Path, "file"))) + using (FileSystemWatcher watcher = CreateWatcher(testDirectory.Path, file.Path, unblockHandler)) + { + TestISynchronizeInvoke invoker = new TestISynchronizeInvoke(); + watcher.SynchronizingObject = invoker; + + int internalBufferOperationCapacity = CalculateInternalBufferOperationCapacity(watcher.InternalBufferSize, file.Path); + + Action action = GetAction(unblockHandler, internalBufferOperationCapacity, file.Path); + Action cleanup = GetCleanup(unblockHandler); + + ExpectError(watcher, action, cleanup); + Assert.True(invoker.BeginInvoke_Called); + } + } + + #region Test Helpers + + private FileSystemWatcher CreateWatcher(string testDirectoryPath, string filePath, ManualResetEvent unblockHandler) + { + var watcher = new FileSystemWatcher(testDirectoryPath, Path.GetFileName(filePath)); + + // block the handling thread + watcher.Changed += (o, e) => unblockHandler.WaitOne(); + + return watcher; + } + + private int CalculateInternalBufferOperationCapacity(int internalBufferSize, string filePath) => + internalBufferSize / (17 + Path.GetFileName(filePath).Length); + + private Action GetAction(ManualResetEvent unblockHandler, int internalBufferOperationCapacity, string filePath) + { + return () => + { + // generate enough file change events to overflow the default buffer + for (int i = 1; i < internalBufferOperationCapacity * 10; i++) + { + File.SetLastWriteTime(filePath, DateTime.Now + TimeSpan.FromSeconds(i)); + } + + unblockHandler.Set(); + }; + } + + private Action GetCleanup(ManualResetEvent unblockHandler) => () => unblockHandler.Reset(); + + #endregion } } diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.cs index 21b5b41238161..0486c97b5d2c4 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.cs @@ -52,23 +52,6 @@ public void Site_NonNullSetEnablesRaisingEvents() } } - internal class TestISynchronizeInvoke : ISynchronizeInvoke - { - public bool BeginInvoke_Called; - public Delegate ExpectedDelegate; - - public IAsyncResult BeginInvoke(Delegate method, object[] args) - { - Assert.Equal(ExpectedDelegate, method); - BeginInvoke_Called = true; - return null; - } - - public bool InvokeRequired => true; - public object EndInvoke(IAsyncResult result) => null; - public object Invoke(Delegate method, object[] args) => null; - } - [Fact] public void SynchronizingObject_GetSetRoundtrips() { @@ -85,7 +68,7 @@ public void SynchronizingObject_GetSetRoundtrips() } /// - /// Ensure that the SynchronizeObject is invoked when an event occurs + /// Ensure that the SynchronizingObject is invoked when an event occurs /// [Theory] [InlineData(WatcherChangeTypes.Changed)] @@ -119,7 +102,7 @@ public void SynchronizingObject_CalledOnEvent(WatcherChangeTypes expectedChangeT } /// - /// Ensure that the SynchronizeObject is invoked when an Renamed event occurs + /// Ensure that the SynchronizingObject is invoked when a Renamed event occurs /// [Fact] public void SynchronizingObject_CalledOnRenamed() @@ -131,13 +114,13 @@ public void SynchronizingObject_CalledOnRenamed() { watcher.SynchronizingObject = invoker; watcher.Renamed += dele; - watcher.CallOnRenamed(new RenamedEventArgs(WatcherChangeTypes.Changed, "test", "name", "oldname")); + watcher.CallOnRenamed(new RenamedEventArgs(WatcherChangeTypes.Renamed, "test", "name", "oldname")); Assert.True(invoker.BeginInvoke_Called); } } /// - /// Ensure that the SynchronizeObject is invoked when an Error event occurs + /// Ensure that the SynchronizingObject is invoked when an Error event occurs /// [Fact] public void SynchronizingObject_CalledOnError() diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs index 53c4d71f7a1a7..c40dcc5d21bf8 100644 --- a/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs +++ b/src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.Threading; using Xunit; @@ -385,14 +386,7 @@ public static bool TryErrorEvent(FileSystemWatcher watcher, Action action, Actio if (attemptsCompleted > 1) { // Re-create the watcher to get a clean iteration. - watcher = new FileSystemWatcher() - { - IncludeSubdirectories = watcher.IncludeSubdirectories, - NotifyFilter = watcher.NotifyFilter, - Filter = watcher.Filter, - Path = watcher.Path, - InternalBufferSize = watcher.InternalBufferSize - }; + watcher = RecreateWatcher(watcher); // Most intermittent failures in FSW are caused by either a shortage of resources (e.g. inotify instances) // or by insufficient time to execute (e.g. CI gets bogged down). Immediately re-running a failed test // won't resolve the first issue, so we wait a little while hoping that things clear up for the next run. @@ -464,7 +458,8 @@ private static FileSystemWatcher RecreateWatcher(FileSystemWatcher watcher) IncludeSubdirectories = watcher.IncludeSubdirectories, NotifyFilter = watcher.NotifyFilter, Path = watcher.Path, - InternalBufferSize = watcher.InternalBufferSize + InternalBufferSize = watcher.InternalBufferSize, + SynchronizingObject = watcher.SynchronizingObject, }; foreach (string filter in watcher.Filters) @@ -550,5 +545,25 @@ void AddEvent(WatcherChangeTypes eventType, string dir1, string dir2 = "") } } } + + internal class TestISynchronizeInvoke : ISynchronizeInvoke + { + public bool BeginInvoke_Called; + public Delegate ExpectedDelegate; + + public IAsyncResult BeginInvoke(Delegate method, object[] args) + { + if (ExpectedDelegate != null) + Assert.Equal(ExpectedDelegate, method); + + BeginInvoke_Called = true; + method.DynamicInvoke(args[0], args[1]); + return null; + } + + public bool InvokeRequired => true; + public object EndInvoke(IAsyncResult result) => null; + public object Invoke(Delegate method, object[] args) => null; + } } }