diff --git a/src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs b/src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs index 52da5e643e66..8c41af21a3a9 100644 --- a/src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs +++ b/src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs @@ -140,8 +140,6 @@ private sealed class RunningInstance // The EventStream to listen for events on private SafeEventStreamHandle _eventStream; - // A reference to the RunLoop that we can use to start or stop a Watcher - private CFRunLoopRef _watcherRunLoop; // Callback delegate for the EventStream events private Interop.EventStream.FSEventStreamCallback _callback; @@ -152,7 +150,8 @@ private sealed class RunningInstance // Calling RunLoopStop multiple times SegFaults so protect the call to it private bool _stopping; - private object StopLock => this; + + private ExecutionContext _context; internal RunningInstance( FileSystemWatcher watcher, @@ -165,7 +164,6 @@ internal RunningInstance( Debug.Assert(!cancelToken.IsCancellationRequested); _weakWatcher = new WeakReference(watcher); - _watcherRunLoop = IntPtr.Zero; _fullDirectory = System.IO.Path.GetFullPath(directory); _includeChildren = includeChildren; _filterFlags = filter; @@ -174,16 +172,103 @@ internal RunningInstance( _stopping = false; } - private void CancellationCallback() + private static class StaticWatcherRunLoopManager { - lock (StopLock) + // A reference to the RunLoop that we can use to start or stop a Watcher + private static CFRunLoopRef s_watcherRunLoop = IntPtr.Zero; + + private static int s_scheduledStreamsCount = 0; + + private static readonly object s_lockObject = new object(); + + public static void ScheduleEventStream(SafeEventStreamHandle eventStream) { - if (!_stopping && _watcherRunLoop != IntPtr.Zero) + lock (s_lockObject) { - _stopping = true; + if (s_watcherRunLoop != IntPtr.Zero) + { + // Schedule the EventStream to run on the thread's RunLoop + s_scheduledStreamsCount++; + Interop.EventStream.FSEventStreamScheduleWithRunLoop(eventStream, s_watcherRunLoop, Interop.RunLoop.kCFRunLoopDefaultMode); + return; + } - // Stop the FS event message pump - Interop.RunLoop.CFRunLoopStop(_watcherRunLoop); + Debug.Assert(s_scheduledStreamsCount == 0); + s_scheduledStreamsCount = 1; + var runLoopStarted = new ManualResetEventSlim(); + new Thread(WatchForFileSystemEventsThreadStart) { IsBackground = true }.Start(new object[] { runLoopStarted, eventStream }); + runLoopStarted.Wait(); + } + } + + public static void UnscheduleFromRunLoop(SafeEventStreamHandle eventStream) + { + Debug.Assert(s_watcherRunLoop != IntPtr.Zero); + lock (s_lockObject) + { + if (s_watcherRunLoop != IntPtr.Zero) + { + // Always unschedule the RunLoop before cleaning up + Interop.EventStream.FSEventStreamUnscheduleFromRunLoop(eventStream, s_watcherRunLoop, Interop.RunLoop.kCFRunLoopDefaultMode); + s_scheduledStreamsCount--; + + if (s_scheduledStreamsCount == 0) + { + // Stop the FS event message pump + Interop.RunLoop.CFRunLoopStop(s_watcherRunLoop); + s_watcherRunLoop = IntPtr.Zero; + } + } + } + } + + private static void WatchForFileSystemEventsThreadStart(object args) + { + var inputArgs = (object[])args; + var runLoopStarted = (ManualResetEventSlim)inputArgs[0]; + var _eventStream = (SafeEventStreamHandle)inputArgs[1]; + // Get this thread's RunLoop + IntPtr runLoop = Interop.RunLoop.CFRunLoopGetCurrent(); + s_watcherRunLoop = runLoop; + Debug.Assert(s_watcherRunLoop != IntPtr.Zero); + + // Retain the RunLoop so that it doesn't get moved or cleaned up before we're done with it. + IntPtr retainResult = Interop.CoreFoundation.CFRetain(runLoop); + Debug.Assert(retainResult == runLoop, "CFRetain is supposed to return the input value"); + + // Schedule the EventStream to run on the thread's RunLoop + Interop.EventStream.FSEventStreamScheduleWithRunLoop(_eventStream, runLoop, Interop.RunLoop.kCFRunLoopDefaultMode); + + runLoopStarted.Set(); + try + { + // Start the OS X RunLoop (a blocking call) that will pump file system changes into the callback function + Interop.RunLoop.CFRunLoopRun(); + } + finally + { + lock (s_lockObject) + { + Interop.CoreFoundation.CFRelease(runLoop); + } + } + } + } + + private void CancellationCallback() + { + if (!_stopping && _eventStream != null) + { + _stopping = true; + + try + { + // When we get here, we've requested to stop so cleanup the EventStream and unschedule from the RunLoop + Interop.EventStream.FSEventStreamStop(_eventStream); + } + finally + { + StaticWatcherRunLoopManager.UnscheduleFromRunLoop(_eventStream); } } } @@ -227,6 +312,8 @@ internal unsafe void Start() _callback = new Interop.EventStream.FSEventStreamCallback(FileSystemEventCallback); } + _context = ExecutionContext.Capture(); + // Make sure the OS file buffer(s) are fully flushed so we don't get events from cached I/O Interop.Sys.Sync(); @@ -244,67 +331,17 @@ internal unsafe void Start() throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), _fullDirectory, true); } - // Create and start our watcher thread then wait for the thread to initialize and start - // the RunLoop. We wait for that to prevent this function from returning before the RunLoop - // has a chance to start so that any callers won't race with the background thread's initialization - // and calling Stop, which would attempt to stop a RunLoop that hasn't started yet. - var runLoopStarted = new ManualResetEventSlim(); - new Thread(WatchForFileSystemEventsThreadStart) { IsBackground = true }.Start(runLoopStarted); - runLoopStarted.Wait(); - } - - private void WatchForFileSystemEventsThreadStart(object arg) - { - var runLoopStarted = (ManualResetEventSlim)arg; - - // Get this thread's RunLoop - _watcherRunLoop = Interop.RunLoop.CFRunLoopGetCurrent(); - Debug.Assert(_watcherRunLoop != IntPtr.Zero); - - // Retain the RunLoop so that it doesn't get moved or cleaned up before we're done with it. - IntPtr retainResult = Interop.CoreFoundation.CFRetain(_watcherRunLoop); - Debug.Assert(retainResult == _watcherRunLoop, "CFRetain is supposed to return the input value"); - - // Schedule the EventStream to run on the thread's RunLoop - Interop.EventStream.FSEventStreamScheduleWithRunLoop(_eventStream, _watcherRunLoop, Interop.RunLoop.kCFRunLoopDefaultMode); - - try - { - bool started = Interop.EventStream.FSEventStreamStart(_eventStream); - - // Notify the StartRaisingEvents call that we are initialized and about to start - // so that it can return and avoid a race-condition around multiple threads calling Stop and Start - runLoopStarted.Set(); + StaticWatcherRunLoopManager.ScheduleEventStream(_eventStream); - if (started) - { - // Start the OS X RunLoop (a blocking call) that will pump file system changes into the callback function - Interop.RunLoop.CFRunLoopRun(); - - // When we get here, we've requested to stop so cleanup the EventStream and unschedule from the RunLoop - Interop.EventStream.FSEventStreamStop(_eventStream); - } - else + bool started = Interop.EventStream.FSEventStreamStart(_eventStream); + if (!started) + { + // Try to get the Watcher to raise the error event; if we can't do that, just silently exit since the watcher is gone anyway + FileSystemWatcher watcher; + if (_weakWatcher.TryGetTarget(out watcher)) { - // Try to get the Watcher to raise the error event; if we can't do that, just silently exist since the watcher is gone anyway - FileSystemWatcher watcher; - if (_weakWatcher.TryGetTarget(out watcher)) - { - // An error occurred while trying to start the run loop so fail out - watcher.OnError(new ErrorEventArgs(new IOException(SR.EventStream_FailedToStart, Marshal.GetLastWin32Error()))); - } - } - } - finally - { - // Always unschedule the RunLoop before cleaning up - Interop.EventStream.FSEventStreamUnscheduleFromRunLoop(_eventStream, _watcherRunLoop, Interop.RunLoop.kCFRunLoopDefaultMode); - - // Release the WatcherLoop Core Foundation object. - lock (StopLock) - { - Interop.CoreFoundation.CFRelease(_watcherRunLoop); - _watcherRunLoop = IntPtr.Zero; + // An error occurred while trying to start the run loop so fail out + watcher.OnError(new ErrorEventArgs(new IOException(SR.EventStream_FailedToStart, Marshal.GetLastWin32Error()))); } } } @@ -332,127 +369,133 @@ private unsafe void FileSystemEventCallback( return; } - // Since renames come in pairs, when we find the first we need to search for the next one. Once we find it, we'll add it to this - // list so when the for-loop comes across it, we'll skip it since it's already been processed as part of the original of the pair. - List handledRenameEvents = null; - Memory[] events = new Memory[numEvents.ToInt32()]; - ProcessEvents(); - - for (long i = 0; i < numEvents.ToInt32(); i++) + ExecutionContext.Run(_context, delegate { - ReadOnlySpan path = events[i].Span; - Debug.Assert(path[path.Length - 1] != '/', "Trailing slashes on events is not supported"); - // Match Windows and don't notify us about changes to the Root folder - if (_fullDirectory.Length >= path.Length && path.Equals(_fullDirectory.AsSpan(0, path.Length), StringComparison.OrdinalIgnoreCase)) - { - continue; - } + // Since renames come in pairs, when we find the first we need to search for the next one. Once we find it, we'll add it to this + // list so when the for-loop comes across it, we'll skip it since it's already been processed as part of the original of the pair. + List handledRenameEvents = null; + Memory[] events = new Memory[numEvents.ToInt32()]; + ProcessEvents(); - WatcherChangeTypes eventType = 0; - // First, we should check if this event should kick off a re-scan since we can't really rely on anything after this point if that is true - if (ShouldRescanOccur(eventFlags[i])) - { - watcher.OnError(new ErrorEventArgs(new IOException(SR.FSW_BufferOverflow, (int)eventFlags[i]))); - break; - } - else if ((handledRenameEvents != null) && (handledRenameEvents.Contains(eventIds[i]))) + for (long i = 0; i < numEvents.ToInt32(); i++) { - // If this event is the second in a rename pair then skip it - continue; - } - else if (CheckIfPathIsNested(path) && ((eventType = FilterEvents(eventFlags[i])) != 0)) - { - // The base FileSystemWatcher does a match check against the relative path before combining with - // the root dir; however, null is special cased to signify the root dir, so check if we should use that. - ReadOnlySpan relativePath = ReadOnlySpan.Empty; - if (!path.Equals(_fullDirectory, StringComparison.OrdinalIgnoreCase)) - { - // Remove the root directory to get the relative path - relativePath = path.Slice(_fullDirectory.Length); - } + ReadOnlySpan path = events[i].Span; + Debug.Assert(path[path.Length - 1] != '/', "Trailing slashes on events is not supported"); - // Raise a notification for the event - if (((eventType & WatcherChangeTypes.Changed) > 0)) + // Match Windows and don't notify us about changes to the Root folder + if (_fullDirectory.Length >= path.Length && path.Equals(_fullDirectory.AsSpan(0, path.Length), StringComparison.OrdinalIgnoreCase)) { - watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Changed, relativePath); + continue; } - if (((eventType & WatcherChangeTypes.Created) > 0)) + + WatcherChangeTypes eventType = 0; + // First, we should check if this event should kick off a re-scan since we can't really rely on anything after this point if that is true + if (ShouldRescanOccur(eventFlags[i])) { - watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Created, relativePath); + watcher.OnError(new ErrorEventArgs(new IOException(SR.FSW_BufferOverflow, (int)eventFlags[i]))); + break; } - if (((eventType & WatcherChangeTypes.Deleted) > 0)) + else if ((handledRenameEvents != null) && (handledRenameEvents.Contains(eventIds[i]))) { - watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, relativePath); + // If this event is the second in a rename pair then skip it + continue; } - if (((eventType & WatcherChangeTypes.Renamed) > 0)) + else if (CheckIfPathIsNested(path) && ((eventType = FilterEvents(eventFlags[i])) != 0)) { - // Find the rename that is paired to this rename, which should be the next rename in the list - long pairedId = FindRenameChangePairedChange(i, eventFlags); - if (pairedId == long.MinValue) + // The base FileSystemWatcher does a match check against the relative path before combining with + // the root dir; however, null is special cased to signify the root dir, so check if we should use that. + ReadOnlySpan relativePath = ReadOnlySpan.Empty; + if (!path.Equals(_fullDirectory, StringComparison.OrdinalIgnoreCase)) { - // Getting here means we have a rename without a pair, meaning it should be a create for the - // move from unwatched folder to watcher folder scenario or a move from the watcher folder out. - // Check if the item exists on disk to check which it is - // Don't send a new notification if we already sent one for this event. - if (DoesItemExist(path, IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemIsFile))) + // Remove the root directory to get the relative path + relativePath = path.Slice(_fullDirectory.Length); + } + + // Raise a notification for the event + if (((eventType & WatcherChangeTypes.Changed) > 0)) + { + watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Changed, relativePath); + } + if (((eventType & WatcherChangeTypes.Created) > 0)) + { + watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Created, relativePath); + } + if (((eventType & WatcherChangeTypes.Deleted) > 0)) + { + watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, relativePath); + } + if (((eventType & WatcherChangeTypes.Renamed) > 0)) + { + // Find the rename that is paired to this rename, which should be the next rename in the list + long pairedId = FindRenameChangePairedChange(i, eventFlags); + if (pairedId == long.MinValue) { - if ((eventType & WatcherChangeTypes.Created) == 0) + // Getting here means we have a rename without a pair, meaning it should be a create for the + // move from unwatched folder to watcher folder scenario or a move from the watcher folder out. + // Check if the item exists on disk to check which it is + // Don't send a new notification if we already sent one for this event. + if (DoesItemExist(path, IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemIsFile))) + { + if ((eventType & WatcherChangeTypes.Created) == 0) + { + watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Created, relativePath); + } + } + else if ((eventType & WatcherChangeTypes.Deleted) == 0) { - watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Created, relativePath); + watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, relativePath); } } - else if ((eventType & WatcherChangeTypes.Deleted) == 0) + else { - watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, relativePath); - } - } - else - { - // Remove the base directory prefix and add the paired event to the list of - // events to skip and notify the user of the rename - ReadOnlySpan newPathRelativeName = events[pairedId].Span.Slice(_fullDirectory.Length); - watcher.NotifyRenameEventArgs(WatcherChangeTypes.Renamed, newPathRelativeName, relativePath); + // Remove the base directory prefix and add the paired event to the list of + // events to skip and notify the user of the rename + ReadOnlySpan newPathRelativeName = events[pairedId].Span.Slice(_fullDirectory.Length); + watcher.NotifyRenameEventArgs(WatcherChangeTypes.Renamed, newPathRelativeName, relativePath); - // Create a new list, if necessary, and add the event - if (handledRenameEvents == null) - { - handledRenameEvents = new List(); + // Create a new list, if necessary, and add the event + if (handledRenameEvents == null) + { + handledRenameEvents = new List(); + } + handledRenameEvents.Add(eventIds[pairedId]); } - handledRenameEvents.Add(eventIds[pairedId]); } } + + ArraySegment underlyingArray; + if (MemoryMarshal.TryGetArray(events[i], out underlyingArray)) + ArrayPool.Shared.Return(underlyingArray.Array); } - ArraySegment underlyingArray; - if (MemoryMarshal.TryGetArray(events[i], out underlyingArray)) - ArrayPool.Shared.Return(underlyingArray.Array); - } + this._context = ExecutionContext.Capture(); - void ProcessEvents() - { - for (int i = 0; i < events.Length; i++) + void ProcessEvents() { - int byteCount = 0; - Debug.Assert(eventPaths[i] != null); - byte* temp = eventPaths[i]; - - // Finds the position of null character. - while(*temp != 0) + for (int i = 0; i < events.Length; i++) { - temp++; - byteCount++; - } + int byteCount = 0; + Debug.Assert(eventPaths[i] != null); + byte* temp = eventPaths[i]; + + // Finds the position of null character. + while(*temp != 0) + { + temp++; + byteCount++; + } - Debug.Assert(byteCount > 0, "Empty events are not supported"); - events[i] = new Memory(ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxCharCount(byteCount))); - int charCount; + Debug.Assert(byteCount > 0, "Empty events are not supported"); + events[i] = new Memory(ArrayPool.Shared.Rent(Encoding.UTF8.GetMaxCharCount(byteCount))); + int charCount; - // Converting an array of bytes to UTF-8 char array - charCount = Encoding.UTF8.GetChars(new ReadOnlySpan(eventPaths[i], byteCount), events[i].Span); - events[i] = events[i].Slice(0, charCount); + // Converting an array of bytes to UTF-8 char array + charCount = Encoding.UTF8.GetChars(new ReadOnlySpan(eventPaths[i], byteCount), events[i].Span); + events[i] = events[i].Slice(0, charCount); + } } - } + }, null); } /// diff --git a/src/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Create.cs b/src/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Create.cs index 41dac32c1f46..aa9d4d000bc9 100644 --- a/src/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Create.cs +++ b/src/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.File.Create.cs @@ -27,38 +27,41 @@ public void FileSystemWatcher_File_Create() } } - [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "#34017")] - [OuterLoop] [Fact] - public void FileSystemWatcher_File_Create_MultipleWatchers_ExecutionContextFlowed() + [OuterLoop] + public void FileSystemWatcher_File_Create_EnablingDisablingNotAffectRaisingEvent() { ExecuteWithRetry(() => - { - using (var watcher1 = new FileSystemWatcher(TestDirectory)) - using (var watcher2 = new FileSystemWatcher(TestDirectory)) + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var watcher = new FileSystemWatcher(testDirectory.Path)) { - string fileName = Path.Combine(TestDirectory, "file"); - watcher1.Filter = Path.GetFileName(fileName); - watcher2.Filter = Path.GetFileName(fileName); + string fileName = Path.Combine(testDirectory.Path, "file"); + watcher.Filter = Path.GetFileName(fileName); - var local = new AsyncLocal(); + int numberOfRaisedEvents = 0; + AutoResetEvent autoResetEvent = new AutoResetEvent(false); + FileSystemEventHandler handler = (o, e) => + { + Interlocked.Increment(ref numberOfRaisedEvents); + autoResetEvent.Set(); + }; - var tcs1 = new TaskCompletionSource(); - var tcs2 = new TaskCompletionSource(); - watcher1.Created += (s, e) => tcs1.SetResult(local.Value); - watcher2.Created += (s, e) => tcs2.SetResult(local.Value); + watcher.Created += handler; - local.Value = 42; - watcher1.EnableRaisingEvents = true; - local.Value = 84; - watcher2.EnableRaisingEvents = true; - local.Value = 168; + for (int i = 0; i < 100; i++) + { + watcher.EnableRaisingEvents = true; + watcher.EnableRaisingEvents = false; + } - File.Create(fileName).Dispose(); - Task.WaitAll(new[] { tcs1.Task, tcs2.Task }, WaitForExpectedEventTimeout); + watcher.EnableRaisingEvents = true; - Assert.Equal(42, tcs1.Task.Result); - Assert.Equal(84, tcs2.Task.Result); + // this should raise one and only one event + File.Create(fileName).Dispose(); + Assert.True(autoResetEvent.WaitOne(WaitForExpectedEventTimeout_NoRetry)); + Assert.False(autoResetEvent.WaitOne(SubsequentExpectedWait)); + Assert.True(numberOfRaisedEvents == 1); } }); } diff --git a/src/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.MultipleWatchers.cs b/src/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.MultipleWatchers.cs new file mode 100644 index 000000000000..b1c3877dd9f8 --- /dev/null +++ b/src/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.MultipleWatchers.cs @@ -0,0 +1,313 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + public class FileSystemWatcher_Multiple_Test : FileSystemWatcherTest + { + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "#34017")] + [OuterLoop] + [Fact] + public void FileSystemWatcher_File_Create_ExecutionContextFlowed() + { + ExecuteWithRetry(() => + { + using (var watcher1 = new FileSystemWatcher(TestDirectory)) + using (var watcher2 = new FileSystemWatcher(TestDirectory)) + { + string fileName = Path.Combine(TestDirectory, "file"); + watcher1.Filter = Path.GetFileName(fileName); + watcher2.Filter = Path.GetFileName(fileName); + + var local = new AsyncLocal(); + + var tcs1 = new TaskCompletionSource(); + var tcs2 = new TaskCompletionSource(); + watcher1.Created += (s, e) => tcs1.SetResult(local.Value); + watcher2.Created += (s, e) => tcs2.SetResult(local.Value); + + local.Value = 42; + watcher1.EnableRaisingEvents = true; + local.Value = 84; + watcher2.EnableRaisingEvents = true; + local.Value = 168; + + File.Create(fileName).Dispose(); + Task.WaitAll(new[] { tcs1.Task, tcs2.Task }, WaitForExpectedEventTimeout); + + Assert.Equal(42, tcs1.Task.Result); + Assert.Equal(84, tcs2.Task.Result); + } + }); + } + + [OuterLoop] + [Fact] + public void FileSystemWatcher_File_Create_NotAffectEachOther() + { + ExecuteWithRetry(() => + { + using (var watcher1 = new FileSystemWatcher(TestDirectory)) + using (var watcher2 = new FileSystemWatcher(TestDirectory)) + using (var watcher3 = new FileSystemWatcher(TestDirectory)) + { + string fileName = Path.Combine(TestDirectory, "file"); + watcher1.Filter = Path.GetFileName(fileName); + watcher2.Filter = Path.GetFileName(fileName); + watcher3.Filter = Path.GetFileName(fileName); + + AutoResetEvent autoResetEvent1 = WatchCreated(watcher1, new[] { fileName }).EventOccured; + AutoResetEvent autoResetEvent2 = WatchCreated(watcher2, new[] { fileName }).EventOccured; + AutoResetEvent autoResetEvent3 = WatchCreated(watcher3, new[] { fileName }).EventOccured; + + watcher1.EnableRaisingEvents = true; + watcher2.EnableRaisingEvents = true; + watcher3.EnableRaisingEvents = true; + + File.Create(fileName).Dispose(); + Assert.True(WaitHandle.WaitAll(new[] { autoResetEvent1, autoResetEvent2, autoResetEvent3 }, WaitForExpectedEventTimeout_NoRetry)); + + File.Delete(fileName); + watcher1.EnableRaisingEvents = false; + + File.Create(fileName).Dispose(); + Assert.False(autoResetEvent1.WaitOne(WaitForUnexpectedEventTimeout)); + Assert.True(WaitHandle.WaitAll(new[] { autoResetEvent2, autoResetEvent3 }, WaitForExpectedEventTimeout_NoRetry)); + } + }); + } + + [OuterLoop] + [Fact] + public void FileSystemWatcher_File_Create_WatchOwnPath() + { + ExecuteWithRetry(() => + { + using (var dir = new TempDirectory(GetTestFilePath())) + using (var dir1 = new TempDirectory(Path.Combine(dir.Path, "dir1"))) + using (var dir2 = new TempDirectory(Path.Combine(dir.Path, "dir2"))) + using (var watcher1 = new FileSystemWatcher(dir1.Path, "*")) + using (var watcher2 = new FileSystemWatcher(dir2.Path, "*")) + { + string fileName1 = Path.Combine(dir1.Path, "file"); + string fileName2 = Path.Combine(dir2.Path, "file"); + + AutoResetEvent autoResetEvent1 = WatchCreated(watcher1, new[] { fileName1 }).EventOccured; + AutoResetEvent autoResetEvent2 = WatchCreated(watcher2, new[] { fileName2 }).EventOccured; + + watcher1.EnableRaisingEvents = true; + watcher2.EnableRaisingEvents = true; + + File.Create(fileName1).Dispose(); + Assert.True(autoResetEvent1.WaitOne(WaitForExpectedEventTimeout_NoRetry)); + Assert.False(autoResetEvent2.WaitOne(WaitForUnexpectedEventTimeout)); + + File.Create(fileName2).Dispose(); + Assert.True(autoResetEvent2.WaitOne(WaitForExpectedEventTimeout_NoRetry)); + Assert.False(autoResetEvent1.WaitOne(WaitForUnexpectedEventTimeout)); + } + }); + } + + [OuterLoop] + [Theory] + [InlineData(true)] + [InlineData(false)] + public void FileSystemWatcher_File_Create_ForceLoopRestart(bool useExistingWatchers) + { + ExecuteWithRetry(() => + { + FileSystemWatcher[] watchers = new FileSystemWatcher[64]; + FileSystemWatcher[] watchers1 = new FileSystemWatcher[64]; + + try + { + string fileName = Path.Combine(TestDirectory, "file"); + AutoResetEvent[] autoResetEvents = new AutoResetEvent[64]; + for (var i = 0; i < watchers.Length; i++) + { + watchers[i] = new FileSystemWatcher(TestDirectory); + watchers[i].Filter = Path.GetFileName(fileName); + autoResetEvents[i] = WatchCreated(watchers[i], new[] { fileName }).EventOccured; + watchers[i].EnableRaisingEvents = true; + } + + File.Create(fileName).Dispose(); + Assert.True(WaitHandle.WaitAll(autoResetEvents, WaitForExpectedEventTimeout_NoRetry)); + + File.Delete(fileName); + for (var i = 0; i < watchers.Length; i++) + { + watchers[i].EnableRaisingEvents = false; + } + + File.Create(fileName).Dispose(); + Assert.False(WaitHandle.WaitAll(autoResetEvents, WaitForUnexpectedEventTimeout)); + + File.Delete(fileName); + + if (useExistingWatchers) + { + for (var i = 0; i < watchers.Length; i++) + { + watchers[i].EnableRaisingEvents = true; + } + + File.Create(fileName).Dispose(); + Assert.True(WaitHandle.WaitAll(autoResetEvents, WaitForExpectedEventTimeout_NoRetry)); + } + else + { + AutoResetEvent[] autoResetEvents1 = new AutoResetEvent[64]; + for (var i = 0; i < watchers1.Length; i++) + { + watchers1[i] = new FileSystemWatcher(TestDirectory); + watchers1[i].Filter = Path.GetFileName(fileName); + autoResetEvents1[i] = WatchCreated(watchers1[i], new[] { fileName }).EventOccured; + watchers1[i].EnableRaisingEvents = true; + } + + File.Create(fileName).Dispose(); + Assert.True(WaitHandle.WaitAll(autoResetEvents1, WaitForExpectedEventTimeout_NoRetry)); + } + } + finally + { + for (var i = 0; i < watchers.Length; i++) + { + watchers[i]?.Dispose(); + watchers1[i]?.Dispose(); + } + } + }); + } + + [OuterLoop] + [Fact] + public void FileSystemWatcher_File_Changed_NotAffectEachOther() + { + ExecuteWithRetry(() => + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var file = new TempFile(Path.Combine(testDirectory.Path, "file"))) + using (var otherFile = new TempFile(Path.Combine(testDirectory.Path, "otherfile"))) + using (var watcher1 = new FileSystemWatcher(testDirectory.Path, Path.GetFileName(file.Path))) + using (var watcher2 = new FileSystemWatcher(testDirectory.Path, Path.GetFileName(file.Path))) + using (var watcher3 = new FileSystemWatcher(testDirectory.Path, Path.GetFileName(otherFile.Path))) + { + AutoResetEvent autoResetEvent1 = WatchChanged(watcher1, new[] { Path.Combine(testDirectory.Path, "file") }).EventOccured; + AutoResetEvent autoResetEvent2 = WatchChanged(watcher2, new[] { Path.Combine(testDirectory.Path, "file") }).EventOccured; + AutoResetEvent autoResetEvent3 = WatchChanged(watcher3, new[] { Path.Combine(testDirectory.Path, "otherfile") }).EventOccured; + + watcher1.EnableRaisingEvents = true; + watcher2.EnableRaisingEvents = true; + watcher3.EnableRaisingEvents = true; + + Directory.SetLastWriteTime(file.Path, DateTime.Now + TimeSpan.FromSeconds(10)); + Assert.True(WaitHandle.WaitAll(new[] { autoResetEvent1, autoResetEvent2 }, WaitForExpectedEventTimeout_NoRetry)); + Assert.False(autoResetEvent3.WaitOne(WaitForUnexpectedEventTimeout)); + + Directory.SetLastWriteTime(otherFile.Path, DateTime.Now + TimeSpan.FromSeconds(10)); + Assert.False(WaitHandle.WaitAll(new[] { autoResetEvent1, autoResetEvent2 }, WaitForUnexpectedEventTimeout)); + Assert.True(autoResetEvent3.WaitOne(WaitForExpectedEventTimeout_NoRetry)); + + watcher1.EnableRaisingEvents = false; + + Directory.SetLastWriteTime(file.Path, DateTime.Now + TimeSpan.FromSeconds(10)); + Assert.False(WaitHandle.WaitAll(new[] { autoResetEvent1, autoResetEvent3 }, WaitForUnexpectedEventTimeout)); + Assert.True(autoResetEvent2.WaitOne(WaitForExpectedEventTimeout_NoRetry)); + + Directory.SetLastWriteTime(otherFile.Path, DateTime.Now + TimeSpan.FromSeconds(10)); + Assert.False(WaitHandle.WaitAll(new[] { autoResetEvent1, autoResetEvent2 }, WaitForUnexpectedEventTimeout)); + Assert.True(autoResetEvent3.WaitOne(WaitForExpectedEventTimeout_NoRetry)); + } + }); + } + + [OuterLoop] + [Fact] + public void FileSystemWatcher_File_Delet_NotAffectEachOther() + { + ExecuteWithRetry(() => + { + using (var watcher1 = new FileSystemWatcher(TestDirectory)) + using (var watcher2 = new FileSystemWatcher(TestDirectory)) + using (var watcher3 = new FileSystemWatcher(TestDirectory)) + { + string fileName = Path.Combine(TestDirectory, "file"); + File.Create(fileName).Dispose(); + + watcher1.Filter = Path.GetFileName(fileName); + watcher2.Filter = Path.GetFileName(fileName); + watcher3.Filter = Path.GetFileName(fileName); + + AutoResetEvent autoResetEvent1 = WatchDeleted(watcher1, new[] { fileName }).EventOccured; + AutoResetEvent autoResetEvent2 = WatchDeleted(watcher2, new[] { fileName }).EventOccured; + AutoResetEvent autoResetEvent3 = WatchDeleted(watcher3, new[] { fileName }).EventOccured; + + watcher1.EnableRaisingEvents = true; + watcher2.EnableRaisingEvents = true; + watcher3.EnableRaisingEvents = true; + + File.Delete(fileName); + Assert.True(WaitHandle.WaitAll(new[] { autoResetEvent1, autoResetEvent2, autoResetEvent3 }, WaitForExpectedEventTimeout_NoRetry)); + + File.Create(fileName).Dispose(); + watcher1.EnableRaisingEvents = false; + + File.Delete(fileName); + Assert.False(autoResetEvent1.WaitOne(WaitForUnexpectedEventTimeout)); + Assert.True(WaitHandle.WaitAll(new[] { autoResetEvent2, autoResetEvent3 }, WaitForExpectedEventTimeout_NoRetry)); + } + }); + } + + [OuterLoop] + [Fact] + [PlatformSpecific(TestPlatforms.OSX)] + public void FileSystemWatcher_File_Rename_NotAffectEachOther() + { + ExecuteWithRetry(() => + { + using (var testDirectory = new TempDirectory(GetTestFilePath())) + using (var file = new TempFile(Path.Combine(testDirectory.Path, "file"))) + using (var watcher1 = new FileSystemWatcher(testDirectory.Path, Path.GetFileName(file.Path))) + using (var watcher2 = new FileSystemWatcher(testDirectory.Path, Path.GetFileName(file.Path))) + { + AutoResetEvent autoResetEvent1_created = WatchCreated(watcher1, new[] { Path.Combine(testDirectory.Path, "file") }).EventOccured; + AutoResetEvent autoResetEvent1_deleted = WatchDeleted(watcher1, new[] { Path.Combine(testDirectory.Path, "file") }).EventOccured; + AutoResetEvent autoResetEvent2_created = WatchCreated(watcher2, new[] { Path.Combine(testDirectory.Path, "file") }).EventOccured; + AutoResetEvent autoResetEvent2_deleted = WatchDeleted(watcher2, new[] { Path.Combine(testDirectory.Path, "file") }).EventOccured; + + watcher1.EnableRaisingEvents = true; + watcher2.EnableRaisingEvents = true; + + string filePath = file.Path; + string filePathRenamed = file.Path + "_renamed"; + + File.Move(filePath, filePathRenamed); + Assert.True(WaitHandle.WaitAll( + new[] { autoResetEvent1_created, autoResetEvent1_deleted, autoResetEvent2_created, autoResetEvent2_deleted }, + WaitForExpectedEventTimeout_NoRetry)); + + File.Move(filePathRenamed, filePath); + watcher1.EnableRaisingEvents = false; + + File.Move(filePath, filePathRenamed); + Assert.False(WaitHandle.WaitAll( + new[] { autoResetEvent1_created, autoResetEvent1_deleted }, + WaitForUnexpectedEventTimeout)); + Assert.True(WaitHandle.WaitAll( + new[] { autoResetEvent2_created, autoResetEvent2_deleted }, + WaitForExpectedEventTimeout_NoRetry)); + } + }); + } + } +} \ No newline at end of file diff --git a/src/System.IO.FileSystem.Watcher/tests/System.IO.FileSystem.Watcher.Tests.csproj b/src/System.IO.FileSystem.Watcher/tests/System.IO.FileSystem.Watcher.Tests.csproj index 62c82591f811..f672f27cdc96 100644 --- a/src/System.IO.FileSystem.Watcher/tests/System.IO.FileSystem.Watcher.Tests.csproj +++ b/src/System.IO.FileSystem.Watcher/tests/System.IO.FileSystem.Watcher.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/src/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs b/src/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs index 61991fcb5848..930010929ed1 100644 --- a/src/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs +++ b/src/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs @@ -23,6 +23,7 @@ public abstract partial class FileSystemWatcherTest : FileCleanupTestBase public const int LongWaitTimeout = 50000; // ms to wait for an event that takes a longer time than the average operation public const int SubsequentExpectedWait = 10; // ms to wait for checks that occur after the first. public const int WaitForExpectedEventTimeout_NoRetry = 3000;// ms to wait for an event that isn't surrounded by a retry. + public const int WaitForUnexpectedEventTimeout = 150; // ms to wait for a non-expected event. public const int DefaultAttemptsForExpectedEvent = 3; // Number of times an expected event should be retried if failing. public const int DefaultAttemptsForUnExpectedEvent = 2; // Number of times an unexpected event should be retried if failing. public const int RetryDelayMilliseconds = 500; // ms to wait when retrying after failure