зеркало из https://github.com/mono/corefx.git
Backport of changes related to FileSystemWatcher problem in VSMac to mono-2018-08 (#234)
* Bring changes from corefx 33865; updated some related stuff due to a bit old version of FileSystemVersion * Fix build issue with FileSystemWatcher on Windows * Avoid a crash in FileSystemWatcher when UnscheduleFromRunLoop is called from the finalizer on shutdown * Port 34438 "Handle flowsuppressed context in FileSystemWatcher on OSX" from dotnet/corefx * Fix build issues
This commit is contained in:
Родитель
1c7e47f64b
Коммит
8d1b6abd3e
|
@ -82,12 +82,12 @@ internal static partial class Interop
|
|||
/// <param name="eventFlags">The events for the corresponding path.</param>
|
||||
/// <param name="eventIds">The machine-and-disk-drive-unique Event ID for the specific event.</param>
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
internal delegate void FSEventStreamCallback(
|
||||
internal unsafe delegate void FSEventStreamCallback(
|
||||
FSEventStreamRef streamReference,
|
||||
IntPtr clientCallBackInfo,
|
||||
size_t numEvents,
|
||||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)]
|
||||
String[] eventPaths,
|
||||
byte** eventPaths,
|
||||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)]
|
||||
FSEventStreamEventFlags[] eventFlags,
|
||||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)]
|
||||
|
|
|
@ -15,7 +15,7 @@ internal partial class Interop
|
|||
internal static extern unsafe bool ReadDirectoryChangesW(
|
||||
SafeFileHandle hDirectory,
|
||||
byte[] lpBuffer,
|
||||
int nBufferLength,
|
||||
uint nBufferLength,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool bWatchSubtree,
|
||||
int dwNotifyFilter,
|
||||
out int lpBytesReturned,
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
// 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.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Text;
|
||||
|
||||
using CFStringRef = System.IntPtr;
|
||||
using FSEventStreamRef = System.IntPtr;
|
||||
|
@ -146,8 +148,6 @@ namespace System.IO
|
|||
// 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;
|
||||
|
@ -158,7 +158,8 @@ namespace System.IO
|
|||
|
||||
// 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,
|
||||
|
@ -171,7 +172,6 @@ namespace System.IO
|
|||
Debug.Assert(!cancelToken.IsCancellationRequested);
|
||||
|
||||
_weakWatcher = new WeakReference<FileSystemWatcher>(watcher);
|
||||
_watcherRunLoop = IntPtr.Zero;
|
||||
_fullDirectory = System.IO.Path.GetFullPath(directory);
|
||||
_includeChildren = includeChildren;
|
||||
_filterFlags = filter;
|
||||
|
@ -180,21 +180,111 @@ namespace System.IO
|
|||
_stopping = false;
|
||||
}
|
||||
|
||||
private void CancellationCallback()
|
||||
private static class StaticWatcherRunLoopManager
|
||||
{
|
||||
lock (StopLock)
|
||||
{
|
||||
if (!_stopping && _watcherRunLoop != IntPtr.Zero)
|
||||
{
|
||||
_stopping = true;
|
||||
// A reference to the RunLoop that we can use to start or stop a Watcher
|
||||
private static CFRunLoopRef s_watcherRunLoop = IntPtr.Zero;
|
||||
|
||||
// Stop the FS event message pump
|
||||
Interop.RunLoop.CFRunLoopStop(_watcherRunLoop);
|
||||
private static int s_scheduledStreamsCount = 0;
|
||||
|
||||
private static readonly object s_lockObject = new object();
|
||||
|
||||
public static void ScheduleEventStream(SafeEventStreamHandle eventStream)
|
||||
{
|
||||
lock (s_lockObject)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
#if MONO
|
||||
s_watcherRunLoop = IntPtr.Zero;
|
||||
#endif
|
||||
Interop.CoreFoundation.CFRelease(runLoop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal void Start()
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal unsafe void Start()
|
||||
{
|
||||
// Make sure _fullPath doesn't contain a link or alias
|
||||
// since the OS will give back the actual, non link'd or alias'd paths
|
||||
|
@ -233,6 +323,8 @@ namespace System.IO
|
|||
_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();
|
||||
|
||||
|
@ -250,82 +342,32 @@ namespace System.IO
|
|||
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();
|
||||
}
|
||||
StaticWatcherRunLoopManager.ScheduleEventStream(_eventStream);
|
||||
|
||||
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();
|
||||
|
||||
if (started)
|
||||
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))
|
||||
{
|
||||
// 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
|
||||
{
|
||||
// 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())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void FileSystemEventCallback(
|
||||
FSEventStreamRef streamRef,
|
||||
IntPtr clientCallBackInfo,
|
||||
size_t numEvents,
|
||||
String[] eventPaths,
|
||||
private unsafe void FileSystemEventCallback(
|
||||
FSEventStreamRef streamRef,
|
||||
IntPtr clientCallBackInfo,
|
||||
size_t numEvents,
|
||||
byte** eventPaths,
|
||||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)]
|
||||
Interop.EventStream.FSEventStreamEventFlags[] eventFlags,
|
||||
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)]
|
||||
FSEventStreamEventId[] eventIds)
|
||||
{
|
||||
Debug.Assert((numEvents.ToInt32() == eventPaths.Length) && (numEvents.ToInt32() == eventFlags.Length) && (numEvents.ToInt32() == eventIds.Length));
|
||||
Debug.Assert((eventPaths != null) && (numEvents.ToInt32() == eventFlags.Length) && (numEvents.ToInt32() == eventIds.Length));
|
||||
|
||||
// Try to get the actual watcher from our weak reference. We maintain a weak reference most of the time
|
||||
// so as to avoid a rooted cycle that would prevent our processing loop from ever ending
|
||||
|
@ -338,18 +380,40 @@ namespace System.IO
|
|||
return;
|
||||
}
|
||||
|
||||
ExecutionContext context = _context;
|
||||
if (context is null)
|
||||
{
|
||||
// Flow suppressed, just run here
|
||||
ProcessEvents(numEvents.ToInt32(), eventPaths, eventFlags, eventIds, watcher);
|
||||
}
|
||||
else
|
||||
{
|
||||
ExecutionContext.Run(
|
||||
context,
|
||||
(object o) => ((RunningInstance)o).ProcessEvents(numEvents.ToInt32(), eventPaths, eventFlags, eventIds, watcher),
|
||||
this);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ProcessEvents(int numEvents,
|
||||
byte** eventPaths,
|
||||
Interop.EventStream.FSEventStreamEventFlags[] eventFlags,
|
||||
FSEventStreamEventId[] eventIds,
|
||||
FileSystemWatcher watcher)
|
||||
{
|
||||
// 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<FSEventStreamEventId> handledRenameEvents = null;
|
||||
Memory<char>[] events = new Memory<char>[numEvents];
|
||||
ParseEvents();
|
||||
|
||||
for (long i = 0; i < numEvents.ToInt32(); i++)
|
||||
for (long i = 0; i < numEvents; i++)
|
||||
{
|
||||
Debug.Assert(eventPaths[i].Length > 0, "Empty events are not supported");
|
||||
Debug.Assert(eventPaths[i][eventPaths[i].Length - 1] != '/', "Trailing slashes on events is not supported");
|
||||
ReadOnlySpan<char> 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
|
||||
string path = eventPaths[i];
|
||||
if (string.Compare(path, 0, _fullDirectory, 0, path.Length, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
if (_fullDirectory.Length >= path.Length && path.Equals(_fullDirectory.AsSpan(0, path.Length), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
@ -366,15 +430,15 @@ namespace System.IO
|
|||
// If this event is the second in a rename pair then skip it
|
||||
continue;
|
||||
}
|
||||
else if (CheckIfPathIsNested(path) && ((eventType = FilterEvents(eventFlags[i], path)) != 0))
|
||||
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.
|
||||
string relativePath = null;
|
||||
if (path.Equals(_fullDirectory, StringComparison.OrdinalIgnoreCase) == false)
|
||||
ReadOnlySpan<char> relativePath = ReadOnlySpan<char>.Empty;
|
||||
if (!path.Equals(_fullDirectory, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Remove the root directory to get the relative path
|
||||
relativePath = path.Remove(0, _fullDirectory.Length);
|
||||
relativePath = path.Slice(_fullDirectory.Length);
|
||||
}
|
||||
|
||||
// Raise a notification for the event
|
||||
|
@ -393,7 +457,7 @@ namespace System.IO
|
|||
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, eventPaths, eventFlags, eventIds);
|
||||
long pairedId = FindRenameChangePairedChange(i, eventFlags);
|
||||
if (pairedId == long.MinValue)
|
||||
{
|
||||
// Getting here means we have a rename without a pair, meaning it should be a create for the
|
||||
|
@ -416,7 +480,7 @@ namespace System.IO
|
|||
{
|
||||
// Remove the base directory prefix and add the paired event to the list of
|
||||
// events to skip and notify the user of the rename
|
||||
string newPathRelativeName = eventPaths[pairedId].Remove(0, _fullDirectory.Length);
|
||||
ReadOnlySpan<char> newPathRelativeName = events[pairedId].Span.Slice(_fullDirectory.Length);
|
||||
watcher.NotifyRenameEventArgs(WatcherChangeTypes.Renamed, newPathRelativeName, relativePath);
|
||||
|
||||
// Create a new list, if necessary, and add the event
|
||||
|
@ -428,6 +492,37 @@ namespace System.IO
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArraySegment<char> underlyingArray;
|
||||
if (MemoryMarshal.TryGetArray(events[i], out underlyingArray))
|
||||
ArrayPool<char>.Shared.Return(underlyingArray.Array);
|
||||
}
|
||||
|
||||
this._context = ExecutionContext.Capture();
|
||||
|
||||
void ParseEvents()
|
||||
{
|
||||
for (int i = 0; i < events.Length; i++)
|
||||
{
|
||||
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<char>(ArrayPool<char>.Shared.Rent(Encoding.UTF8.GetMaxCharCount(byteCount)));
|
||||
int charCount;
|
||||
|
||||
// Converting an array of bytes to UTF-8 char array
|
||||
charCount = Encoding.UTF8.GetChars(new ReadOnlySpan<byte>(eventPaths[i], byteCount), events[i].Span);
|
||||
events[i] = events[i].Slice(0, charCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -435,7 +530,7 @@ namespace System.IO
|
|||
/// Compares the given event flags to the filter flags and returns which event (if any) corresponds
|
||||
/// to those flags.
|
||||
/// </summary>
|
||||
private WatcherChangeTypes FilterEvents(Interop.EventStream.FSEventStreamEventFlags eventFlags, string fullPath)
|
||||
private WatcherChangeTypes FilterEvents(Interop.EventStream.FSEventStreamEventFlags eventFlags)
|
||||
{
|
||||
const Interop.EventStream.FSEventStreamEventFlags changedFlags = Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemInodeMetaMod |
|
||||
Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemFinderInfoMod |
|
||||
|
@ -487,30 +582,24 @@ namespace System.IO
|
|||
IsFlagSet(flags, Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagUnmount));
|
||||
}
|
||||
|
||||
private bool CheckIfPathIsNested(string eventPath)
|
||||
private bool CheckIfPathIsNested(ReadOnlySpan<char> eventPath)
|
||||
{
|
||||
bool doesPathPass = true;
|
||||
|
||||
// If we shouldn't include subdirectories, check if this path's parent is the watch directory
|
||||
if (_includeChildren == false)
|
||||
{
|
||||
// Check if the parent is the root. If so, then we'll continue processing based on the name.
|
||||
// If it isn't, then this will be set to false and we'll skip the name processing since it's irrelevant.
|
||||
string parent = System.IO.Path.GetDirectoryName(eventPath);
|
||||
doesPathPass = (string.Compare(parent, 0, _fullDirectory, 0, parent.Length, StringComparison.OrdinalIgnoreCase) == 0);
|
||||
}
|
||||
|
||||
return doesPathPass;
|
||||
// Check if the parent is the root. If so, then we'll continue processing based on the name.
|
||||
// If it isn't, then this will be set to false and we'll skip the name processing since it's irrelevant.
|
||||
#if MONO
|
||||
return _includeChildren || _fullDirectory.AsSpan().StartsWith(System.IO.Path.GetDirectoryName(eventPath.ToString()), StringComparison.OrdinalIgnoreCase);
|
||||
#else
|
||||
return _includeChildren || _fullDirectory.AsSpan().StartsWith(System.IO.Path.GetDirectoryName(eventPath), StringComparison.OrdinalIgnoreCase);
|
||||
#endif
|
||||
}
|
||||
|
||||
private long FindRenameChangePairedChange(
|
||||
long currentIndex,
|
||||
String[] eventPaths,
|
||||
Interop.EventStream.FSEventStreamEventFlags[] eventFlags,
|
||||
FSEventStreamEventId[] eventIds)
|
||||
Interop.EventStream.FSEventStreamEventFlags[] eventFlags)
|
||||
{
|
||||
// Start at one past the current index and try to find the next Rename item, which should be the old path.
|
||||
for (long i = currentIndex + 1; i < eventPaths.Length; i++)
|
||||
for (long i = currentIndex + 1; i < eventFlags.Length; i++)
|
||||
{
|
||||
if (IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemRenamed))
|
||||
{
|
||||
|
@ -527,12 +616,26 @@ namespace System.IO
|
|||
return (value & flags) == value;
|
||||
}
|
||||
|
||||
private static bool DoesItemExist(string path, bool isFile)
|
||||
private static bool DoesItemExist(ReadOnlySpan<char> path, bool isFile)
|
||||
{
|
||||
if (isFile)
|
||||
return File.Exists(path);
|
||||
else
|
||||
return Directory.Exists(path);
|
||||
if (path.IsEmpty || path.Length == 0)
|
||||
return false;
|
||||
|
||||
#if MONO
|
||||
if (!isFile)
|
||||
return Directory.Exists(path.ToString());
|
||||
|
||||
return path[path.Length - 1] == '/'
|
||||
? false
|
||||
: File.Exists(path.ToString());
|
||||
#else
|
||||
if (!isFile)
|
||||
return FileSystem.DirectoryExists(path);
|
||||
|
||||
return PathInternal.IsDirectorySeparator(path[path.Length - 1])
|
||||
? false
|
||||
: FileSystem.FileExists(path);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
// 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;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
@ -25,15 +28,12 @@ namespace System.IO
|
|||
/// </devdoc>
|
||||
public partial class FileSystemWatcher : Component, ISupportInitialize
|
||||
{
|
||||
/// <devdoc>
|
||||
/// Private instance variables
|
||||
/// </devdoc>
|
||||
// Filters collection
|
||||
private readonly NormalizedFilterCollection _filters = new NormalizedFilterCollection();
|
||||
|
||||
// Directory being monitored
|
||||
private string _directory;
|
||||
|
||||
// Filter for name matching
|
||||
private string _filter;
|
||||
|
||||
// The watch filter for the API call.
|
||||
private const NotifyFilters c_defaultNotifyFilters = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName;
|
||||
private NotifyFilters _notifyFilters = c_defaultNotifyFilters;
|
||||
|
@ -48,11 +48,10 @@ namespace System.IO
|
|||
private bool _initializing = false;
|
||||
|
||||
// Buffer size
|
||||
private int _internalBufferSize = 8192;
|
||||
private uint _internalBufferSize = 8192;
|
||||
|
||||
// Used for synchronization
|
||||
private bool _disposed;
|
||||
private ISynchronizeInvoke _synchronizingObject;
|
||||
|
||||
// Event handlers
|
||||
private FileSystemEventHandler _onChangedHandler = null;
|
||||
|
@ -89,15 +88,16 @@ namespace System.IO
|
|||
public FileSystemWatcher()
|
||||
{
|
||||
_directory = string.Empty;
|
||||
_filter = "*";
|
||||
}
|
||||
|
||||
/// <devdoc>
|
||||
/// Initializes a new instance of the <see cref='System.IO.FileSystemWatcher'/> class,
|
||||
/// given the specified directory to monitor.
|
||||
/// </devdoc>
|
||||
public FileSystemWatcher(string path) : this(path, "*")
|
||||
public FileSystemWatcher(string path)
|
||||
{
|
||||
CheckPathValidity(path);
|
||||
_directory = path;
|
||||
}
|
||||
|
||||
/// <devdoc>
|
||||
|
@ -106,21 +106,9 @@ namespace System.IO
|
|||
/// </devdoc>
|
||||
public FileSystemWatcher(string path, string filter)
|
||||
{
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
|
||||
// Early check for directory parameter so that an exception can be thrown as early as possible.
|
||||
if (path.Length == 0)
|
||||
throw new ArgumentException(SR.Format(SR.InvalidDirName, path), nameof(path));
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
throw new ArgumentException(SR.Format(SR.InvalidDirName_NotExists, path), nameof(path));
|
||||
|
||||
CheckPathValidity(path);
|
||||
_directory = path;
|
||||
_filter = filter ?? throw new ArgumentNullException(nameof(filter));
|
||||
|
||||
if (_filter == "*.*")
|
||||
_filter = "*";
|
||||
Filter = filter ?? throw new ArgumentNullException(nameof(filter));
|
||||
}
|
||||
|
||||
/// <devdoc>
|
||||
|
@ -146,6 +134,8 @@ namespace System.IO
|
|||
}
|
||||
}
|
||||
|
||||
public Collection<string> Filters => _filters;
|
||||
|
||||
/// <devdoc>
|
||||
/// Gets or sets a value indicating whether the component is enabled.
|
||||
/// </devdoc>
|
||||
|
@ -187,20 +177,12 @@ namespace System.IO
|
|||
{
|
||||
get
|
||||
{
|
||||
return _filter;
|
||||
return Filters.Count == 0 ? "*" : Filters[0];
|
||||
}
|
||||
set
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
// Skip the string compare for "*" since it has no case-insensitive representation that differs from
|
||||
// the case-sensitive representation.
|
||||
_filter = "*";
|
||||
}
|
||||
else if (!string.Equals(_filter, value, PathInternal.StringComparison))
|
||||
{
|
||||
_filter = value == "*.*" ? "*" : value;
|
||||
}
|
||||
Filters.Clear();
|
||||
Filters.Add(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,7 +213,7 @@ namespace System.IO
|
|||
{
|
||||
get
|
||||
{
|
||||
return _internalBufferSize;
|
||||
return (int)_internalBufferSize;
|
||||
}
|
||||
set
|
||||
{
|
||||
|
@ -243,7 +225,7 @@ namespace System.IO
|
|||
}
|
||||
else
|
||||
{
|
||||
_internalBufferSize = value;
|
||||
_internalBufferSize = (uint)value;
|
||||
}
|
||||
|
||||
Restart();
|
||||
|
@ -284,7 +266,7 @@ namespace System.IO
|
|||
|
||||
if (!Directory.Exists(value))
|
||||
throw new ArgumentException(SR.Format(SR.InvalidDirName_NotExists, value), nameof(Path));
|
||||
|
||||
|
||||
_directory = value;
|
||||
Restart();
|
||||
}
|
||||
|
@ -367,8 +349,6 @@ namespace System.IO
|
|||
}
|
||||
}
|
||||
|
||||
/// <devdoc>
|
||||
/// </devdoc>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
try
|
||||
|
@ -398,66 +378,100 @@ namespace System.IO
|
|||
}
|
||||
}
|
||||
|
||||
/// <devdoc>
|
||||
/// Sees if the name given matches the name filter we have.
|
||||
/// </devdoc>
|
||||
/// <internalonly/>
|
||||
private bool MatchPattern(string relativePath)
|
||||
private static void CheckPathValidity(string path)
|
||||
{
|
||||
ReadOnlySpan<char> name = IO.Path.GetFileName(relativePath.AsSpan());
|
||||
return name.Length > 0
|
||||
? FileSystemName.MatchesSimpleExpression(_filter, name, ignoreCase: !PathInternal.IsCaseSensitive)
|
||||
: false;
|
||||
if (path == null)
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
|
||||
// Early check for directory parameter so that an exception can be thrown as early as possible.
|
||||
if (path.Length == 0)
|
||||
throw new ArgumentException(SR.Format(SR.InvalidDirName, path), nameof(path));
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
throw new ArgumentException(SR.Format(SR.InvalidDirName_NotExists, path), nameof(path));
|
||||
}
|
||||
|
||||
/// <devdoc>
|
||||
/// Raises the event to each handler in the list.
|
||||
/// </devdoc>
|
||||
/// <internalonly/>
|
||||
/// <summary>
|
||||
/// Sees if the name given matches the name filter we have.
|
||||
/// </summary>
|
||||
private bool MatchPattern(ReadOnlySpan<char> relativePath)
|
||||
{
|
||||
ReadOnlySpan<char> name = IO.Path.GetFileName(relativePath);
|
||||
if (name.Length == 0)
|
||||
return false;
|
||||
|
||||
string[] filters = _filters.GetFilters();
|
||||
if (filters.Length == 0)
|
||||
return true;
|
||||
|
||||
foreach (string filter in filters)
|
||||
{
|
||||
if (FileSystemName.MatchesSimpleExpression(filter, name, ignoreCase: !PathInternal.IsCaseSensitive))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the event to each handler in the list.
|
||||
/// </summary>
|
||||
private void NotifyInternalBufferOverflowEvent()
|
||||
{
|
||||
_onErrorHandler?.Invoke(this, new ErrorEventArgs(
|
||||
new InternalBufferOverflowException(SR.Format(SR.FSW_BufferOverflow, _directory))));
|
||||
}
|
||||
|
||||
/// <devdoc>
|
||||
/// Raises the event to each handler in the list.
|
||||
/// </devdoc>
|
||||
/// <internalonly/>
|
||||
private void NotifyRenameEventArgs(WatcherChangeTypes action, string name, string oldName)
|
||||
/// <summary>
|
||||
/// Raises the event to each handler in the list.
|
||||
/// </summary>
|
||||
private void NotifyRenameEventArgs(WatcherChangeTypes action, ReadOnlySpan<char> name, ReadOnlySpan<char> oldName)
|
||||
{
|
||||
// filter if there's no handler or neither new name or old name match a specified pattern
|
||||
RenamedEventHandler handler = _onRenamedHandler;
|
||||
if (handler != null &&
|
||||
(MatchPattern(name) || MatchPattern(oldName)))
|
||||
{
|
||||
handler(this, new RenamedEventArgs(action, _directory, name, oldName));
|
||||
handler(this, new RenamedEventArgs(action, _directory, name.IsEmpty ? null : name.ToString(), oldName.IsEmpty ? null : oldName.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <devdoc>
|
||||
/// Raises the event to each handler in the list.
|
||||
/// </devdoc>
|
||||
/// <internalonly/>
|
||||
private void NotifyFileSystemEventArgs(WatcherChangeTypes changeType, string name)
|
||||
private FileSystemEventHandler GetHandler(WatcherChangeTypes changeType)
|
||||
{
|
||||
FileSystemEventHandler handler = null;
|
||||
switch (changeType)
|
||||
{
|
||||
case WatcherChangeTypes.Created:
|
||||
handler = _onCreatedHandler;
|
||||
break;
|
||||
return _onCreatedHandler;
|
||||
case WatcherChangeTypes.Deleted:
|
||||
handler = _onDeletedHandler;
|
||||
break;
|
||||
return _onDeletedHandler;
|
||||
case WatcherChangeTypes.Changed:
|
||||
handler = _onChangedHandler;
|
||||
break;
|
||||
default:
|
||||
Debug.Fail("Unknown FileSystemEvent change type! Value: " + changeType);
|
||||
break;
|
||||
return _onChangedHandler;
|
||||
}
|
||||
|
||||
Debug.Fail("Unknown FileSystemEvent change type! Value: " + changeType);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the event to each handler in the list.
|
||||
/// </summary>
|
||||
private void NotifyFileSystemEventArgs(WatcherChangeTypes changeType, ReadOnlySpan<char> name)
|
||||
{
|
||||
FileSystemEventHandler handler = GetHandler(changeType);
|
||||
|
||||
if (handler != null && MatchPattern(name.IsEmpty ? _directory : name))
|
||||
{
|
||||
handler(this, new FileSystemEventArgs(changeType, _directory, name.IsEmpty ? null : name.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raises the event to each handler in the list.
|
||||
/// </summary>
|
||||
private void NotifyFileSystemEventArgs(WatcherChangeTypes changeType, string name)
|
||||
{
|
||||
FileSystemEventHandler handler = GetHandler(changeType);
|
||||
|
||||
if (handler != null && MatchPattern(string.IsNullOrEmpty(name) ? _directory : name))
|
||||
{
|
||||
handler(this, new FileSystemEventArgs(changeType, _directory, name));
|
||||
|
@ -560,9 +574,12 @@ namespace System.IO
|
|||
tcs.TrySetResult(new WaitForChangedResult(e.ChangeType, e.Name, oldName: null, timedOut: false));
|
||||
}
|
||||
};
|
||||
if ((changeType & WatcherChangeTypes.Created) != 0) Created += fseh;
|
||||
if ((changeType & WatcherChangeTypes.Deleted) != 0) Deleted += fseh;
|
||||
if ((changeType & WatcherChangeTypes.Changed) != 0) Changed += fseh;
|
||||
if ((changeType & WatcherChangeTypes.Created) != 0)
|
||||
Created += fseh;
|
||||
if ((changeType & WatcherChangeTypes.Deleted) != 0)
|
||||
Deleted += fseh;
|
||||
if ((changeType & WatcherChangeTypes.Changed) != 0)
|
||||
Changed += fseh;
|
||||
}
|
||||
if ((changeType & WatcherChangeTypes.Renamed) != 0)
|
||||
{
|
||||
|
@ -600,9 +617,12 @@ namespace System.IO
|
|||
}
|
||||
if (fseh != null)
|
||||
{
|
||||
if ((changeType & WatcherChangeTypes.Changed) != 0) Changed -= fseh;
|
||||
if ((changeType & WatcherChangeTypes.Deleted) != 0) Deleted -= fseh;
|
||||
if ((changeType & WatcherChangeTypes.Created) != 0) Created -= fseh;
|
||||
if ((changeType & WatcherChangeTypes.Changed) != 0)
|
||||
Changed -= fseh;
|
||||
if ((changeType & WatcherChangeTypes.Deleted) != 0)
|
||||
Deleted -= fseh;
|
||||
if ((changeType & WatcherChangeTypes.Created) != 0)
|
||||
Created -= fseh;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -656,17 +676,7 @@ namespace System.IO
|
|||
}
|
||||
}
|
||||
|
||||
public ISynchronizeInvoke SynchronizingObject
|
||||
{
|
||||
get
|
||||
{
|
||||
return _synchronizingObject;
|
||||
}
|
||||
set
|
||||
{
|
||||
_synchronizingObject = value;
|
||||
}
|
||||
}
|
||||
public ISynchronizeInvoke SynchronizingObject { get; set; }
|
||||
|
||||
public void BeginInit()
|
||||
{
|
||||
|
@ -688,5 +698,101 @@ namespace System.IO
|
|||
{
|
||||
return _initializing || DesignMode;
|
||||
}
|
||||
|
||||
private sealed class NormalizedFilterCollection : Collection<string>
|
||||
{
|
||||
internal NormalizedFilterCollection() : base(new ImmutableStringList())
|
||||
{
|
||||
}
|
||||
|
||||
protected override void InsertItem(int index, string item)
|
||||
{
|
||||
base.InsertItem(index, string.IsNullOrEmpty(item) || item == "*.*" ? "*" : item);
|
||||
}
|
||||
|
||||
protected override void SetItem(int index, string item)
|
||||
{
|
||||
base.SetItem(index, string.IsNullOrEmpty(item) || item == "*.*" ? "*" : item);
|
||||
}
|
||||
|
||||
internal string[] GetFilters() => ((ImmutableStringList)Items).Items;
|
||||
|
||||
/// <summary>
|
||||
/// List that maintains its underlying data in an immutable array, such that the list
|
||||
/// will never modify an array returned from its Items property. This is to allow
|
||||
/// the array to be enumerated safely while another thread might be concurrently mutating
|
||||
/// the collection.
|
||||
/// </summary>
|
||||
private sealed class ImmutableStringList : IList<string>
|
||||
{
|
||||
public string[] Items = Array.Empty<string>();
|
||||
|
||||
public string this[int index]
|
||||
{
|
||||
get
|
||||
{
|
||||
string[] items = Items;
|
||||
if ((uint)index >= (uint)items.Length)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
}
|
||||
return items[index];
|
||||
}
|
||||
set
|
||||
{
|
||||
string[] clone = (string[])Items.Clone();
|
||||
clone[index] = value;
|
||||
Items = clone;
|
||||
}
|
||||
}
|
||||
|
||||
public int Count => Items.Length;
|
||||
|
||||
public bool IsReadOnly => false;
|
||||
|
||||
public void Add(string item)
|
||||
{
|
||||
// Collection<T> doesn't use this method.
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void Clear() => Items = Array.Empty<string>();
|
||||
|
||||
public bool Contains(string item) => Array.IndexOf(Items, item) != -1;
|
||||
|
||||
public void CopyTo(string[] array, int arrayIndex) => Items.CopyTo(array, arrayIndex);
|
||||
|
||||
public IEnumerator<string> GetEnumerator() => ((IEnumerable<string>)Items).GetEnumerator();
|
||||
|
||||
public int IndexOf(string item) => Array.IndexOf(Items, item);
|
||||
|
||||
public void Insert(int index, string item)
|
||||
{
|
||||
string[] items = Items;
|
||||
string[] newItems = new string[items.Length + 1];
|
||||
items.AsSpan(0, index).CopyTo(newItems);
|
||||
items.AsSpan(index).CopyTo(newItems.AsSpan(index + 1));
|
||||
newItems[index] = item;
|
||||
Items = newItems;
|
||||
}
|
||||
|
||||
public bool Remove(string item)
|
||||
{
|
||||
// Collection<T> doesn't use this method.
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
public void RemoveAt(int index)
|
||||
{
|
||||
string[] items = Items;
|
||||
string[] newItems = new string[items.Length - 1];
|
||||
items.AsSpan(0, index).CopyTo(newItems);
|
||||
items.AsSpan(index + 1).CopyTo(newItems.AsSpan(index));
|
||||
Items = newItems;
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,45 @@ namespace System.IO.Tests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[OuterLoop]
|
||||
public void FileSystemWatcher_File_Create_EnablingDisablingNotAffectRaisingEvent()
|
||||
{
|
||||
ExecuteWithRetry(() =>
|
||||
{
|
||||
using (var testDirectory = new TempDirectory(GetTestFilePath()))
|
||||
using (var watcher = new FileSystemWatcher(testDirectory.Path))
|
||||
{
|
||||
string fileName = Path.Combine(testDirectory.Path, "file");
|
||||
watcher.Filter = Path.GetFileName(fileName);
|
||||
|
||||
int numberOfRaisedEvents = 0;
|
||||
AutoResetEvent autoResetEvent = new AutoResetEvent(false);
|
||||
FileSystemEventHandler handler = (o, e) =>
|
||||
{
|
||||
Interlocked.Increment(ref numberOfRaisedEvents);
|
||||
autoResetEvent.Set();
|
||||
};
|
||||
|
||||
watcher.Created += handler;
|
||||
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
watcher.EnableRaisingEvents = true;
|
||||
watcher.EnableRaisingEvents = false;
|
||||
}
|
||||
|
||||
watcher.EnableRaisingEvents = true;
|
||||
|
||||
// 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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FileSystemWatcher_File_Create_ForcedRestart()
|
||||
{
|
||||
|
|
|
@ -0,0 +1,351 @@
|
|||
// 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<int>();
|
||||
|
||||
var tcs1 = new TaskCompletionSource<int>();
|
||||
var tcs2 = new TaskCompletionSource<int>();
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "#34017")]
|
||||
[OuterLoop]
|
||||
[Fact]
|
||||
public void FileSystemWatcher_File_Create_SuppressedExecutionContextHandled()
|
||||
{
|
||||
ExecuteWithRetry(() =>
|
||||
{
|
||||
using (var watcher1 = new FileSystemWatcher(TestDirectory))
|
||||
{
|
||||
string fileName = Path.Combine(TestDirectory, "file");
|
||||
watcher1.Filter = Path.GetFileName(fileName);
|
||||
|
||||
var local = new AsyncLocal<int>();
|
||||
|
||||
var tcs1 = new TaskCompletionSource<int>();
|
||||
watcher1.Created += (s, e) => tcs1.SetResult(local.Value);
|
||||
|
||||
local.Value = 42;
|
||||
|
||||
ExecutionContext.SuppressFlow();
|
||||
try
|
||||
{
|
||||
watcher1.EnableRaisingEvents = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ExecutionContext.RestoreFlow();
|
||||
}
|
||||
|
||||
File.Create(fileName).Dispose();
|
||||
tcs1.Task.Wait(WaitForExpectedEventTimeout);
|
||||
|
||||
Assert.Equal(0, tcs1.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)]
|
||||
[SkipOnTargetFramework(TargetFrameworkMonikers.Mono)]
|
||||
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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -474,7 +474,7 @@ namespace System.IO.Tests
|
|||
using (var file = new TempFile(Path.Combine(dir.Path, "file")))
|
||||
using (var fsw = new FileSystemWatcher(dir.Path))
|
||||
{
|
||||
AutoResetEvent eventOccurred = WatchRenamed(fsw);
|
||||
AutoResetEvent eventOccurred = WatchRenamed(fsw).EventOccured;
|
||||
|
||||
string newPath = Path.Combine(dir.Path, "newPath");
|
||||
|
||||
|
@ -626,7 +626,7 @@ namespace System.IO.Tests
|
|||
using (var dir = new TempDirectory(Path.Combine(testDirectory.Path, "dir")))
|
||||
using (var fsw = new FileSystemWatcher(dir.Path))
|
||||
{
|
||||
AutoResetEvent are = WatchCreated(fsw);
|
||||
AutoResetEvent are = WatchCreated(fsw).EventOccured;
|
||||
|
||||
fsw.Filter = "*";
|
||||
fsw.EnableRaisingEvents = true;
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
<Compile Include="FileSystemWatcher.File.Move.cs" />
|
||||
<Compile Include="FileSystemWatcher.File.NotifyFilter.cs" />
|
||||
<Compile Include="FileSystemWatcher.InternalBufferSize.cs" />
|
||||
<Compile Include="FileSystemWatcher.MultipleWatchers.cs" />
|
||||
<Compile Include="FileSystemWatcher.WaitForChanged.cs" />
|
||||
<Compile Include="FileSystemWatcher.unit.cs" />
|
||||
<!-- Helpers -->
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace System.IO.Tests
|
||||
{
|
||||
public abstract class FileSystemWatcherTest : FileCleanupTestBase
|
||||
public abstract partial class FileSystemWatcherTest : FileCleanupTestBase
|
||||
{
|
||||
// Events are reported asynchronously by the OS, so allow an amount of time for
|
||||
// them to arrive before testing an assertion. If we expect an event to occur,
|
||||
|
@ -23,18 +23,20 @@ namespace System.IO.Tests
|
|||
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
|
||||
|
||||
/// <summary>
|
||||
/// Watches the Changed WatcherChangeType and unblocks the returned AutoResetEvent when a
|
||||
/// Changed event is thrown by the watcher.
|
||||
/// </summary>
|
||||
public static AutoResetEvent WatchChanged(FileSystemWatcher watcher, string[] expectedPaths = null)
|
||||
public static (AutoResetEvent EventOccured, FileSystemEventHandler Handler) WatchChanged(FileSystemWatcher watcher, string[] expectedPaths = null)
|
||||
{
|
||||
AutoResetEvent eventOccurred = new AutoResetEvent(false);
|
||||
|
||||
watcher.Changed += (o, e) =>
|
||||
FileSystemEventHandler changeHandler = (o, e) =>
|
||||
{
|
||||
Assert.Equal(WatcherChangeTypes.Changed, e.ChangeType);
|
||||
if (expectedPaths != null)
|
||||
|
@ -44,18 +46,19 @@ namespace System.IO.Tests
|
|||
eventOccurred.Set();
|
||||
};
|
||||
|
||||
return eventOccurred;
|
||||
watcher.Changed += changeHandler;
|
||||
return (eventOccurred, changeHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Watches the Created WatcherChangeType and unblocks the returned AutoResetEvent when a
|
||||
/// Created event is thrown by the watcher.
|
||||
/// </summary>
|
||||
public static AutoResetEvent WatchCreated(FileSystemWatcher watcher, string[] expectedPaths = null)
|
||||
public static (AutoResetEvent EventOccured, FileSystemEventHandler Handler) WatchCreated(FileSystemWatcher watcher, string[] expectedPaths = null)
|
||||
{
|
||||
AutoResetEvent eventOccurred = new AutoResetEvent(false);
|
||||
|
||||
watcher.Created += (o, e) =>
|
||||
FileSystemEventHandler handler = (o, e) =>
|
||||
{
|
||||
Assert.Equal(WatcherChangeTypes.Created, e.ChangeType);
|
||||
if (expectedPaths != null)
|
||||
|
@ -65,18 +68,18 @@ namespace System.IO.Tests
|
|||
eventOccurred.Set();
|
||||
};
|
||||
|
||||
return eventOccurred;
|
||||
watcher.Created += handler;
|
||||
return (eventOccurred, handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Watches the Renamed WatcherChangeType and unblocks the returned AutoResetEvent when a
|
||||
/// Renamed event is thrown by the watcher.
|
||||
/// </summary>
|
||||
public static AutoResetEvent WatchDeleted(FileSystemWatcher watcher, string[] expectedPaths = null)
|
||||
public static (AutoResetEvent EventOccured, FileSystemEventHandler Handler) WatchDeleted(FileSystemWatcher watcher, string[] expectedPaths = null)
|
||||
{
|
||||
AutoResetEvent eventOccurred = new AutoResetEvent(false);
|
||||
|
||||
watcher.Deleted += (o, e) =>
|
||||
FileSystemEventHandler handler = (o, e) =>
|
||||
{
|
||||
Assert.Equal(WatcherChangeTypes.Deleted, e.ChangeType);
|
||||
if (expectedPaths != null)
|
||||
|
@ -86,18 +89,19 @@ namespace System.IO.Tests
|
|||
eventOccurred.Set();
|
||||
};
|
||||
|
||||
return eventOccurred;
|
||||
watcher.Deleted += handler;
|
||||
return (eventOccurred, handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Watches the Renamed WatcherChangeType and unblocks the returned AutoResetEvent when a
|
||||
/// Renamed event is thrown by the watcher.
|
||||
/// </summary>
|
||||
public static AutoResetEvent WatchRenamed(FileSystemWatcher watcher, string[] expectedPaths = null)
|
||||
public static (AutoResetEvent EventOccured, RenamedEventHandler Handler) WatchRenamed(FileSystemWatcher watcher, string[] expectedPaths = null)
|
||||
{
|
||||
AutoResetEvent eventOccurred = new AutoResetEvent(false);
|
||||
|
||||
watcher.Renamed += (o, e) =>
|
||||
RenamedEventHandler handler = (o, e) =>
|
||||
{
|
||||
Assert.Equal(WatcherChangeTypes.Renamed, e.ChangeType);
|
||||
if (expectedPaths != null)
|
||||
|
@ -107,7 +111,8 @@ namespace System.IO.Tests
|
|||
eventOccurred.Set();
|
||||
};
|
||||
|
||||
return eventOccurred;
|
||||
watcher.Renamed += handler;
|
||||
return (eventOccurred, handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -115,7 +120,7 @@ namespace System.IO.Tests
|
|||
/// </summary>
|
||||
public static void ExpectEvent(WaitHandle eventOccurred, string eventName_NoRetry)
|
||||
{
|
||||
string message = String.Format("Didn't observe a {0} event within {1}ms", eventName_NoRetry, WaitForExpectedEventTimeout_NoRetry);
|
||||
string message = string.Format("Didn't observe a {0} event within {1}ms", eventName_NoRetry, WaitForExpectedEventTimeout_NoRetry);
|
||||
Assert.True(eventOccurred.WaitOne(WaitForExpectedEventTimeout_NoRetry), message);
|
||||
}
|
||||
|
||||
|
@ -161,32 +166,63 @@ namespace System.IO.Tests
|
|||
{
|
||||
int attemptsCompleted = 0;
|
||||
bool result = false;
|
||||
FileSystemWatcher newWatcher = watcher;
|
||||
while (!result && attemptsCompleted++ < attempts)
|
||||
{
|
||||
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
|
||||
};
|
||||
newWatcher = RecreateWatcher(newWatcher);
|
||||
// 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.
|
||||
Thread.Sleep(500);
|
||||
Thread.Sleep(RetryDelayMilliseconds);
|
||||
}
|
||||
|
||||
result = ExecuteAndVerifyEvents(watcher, expectedEvents, action, attemptsCompleted == attempts, expectedPaths, timeout);
|
||||
result = ExecuteAndVerifyEvents(newWatcher, expectedEvents, action, attemptsCompleted == attempts, expectedPaths, timeout);
|
||||
|
||||
if (cleanup != null)
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Invokes the specified test action with retry on failure (other than assertion failure).</summary>
|
||||
/// <param name="action">The test action.</param>
|
||||
/// <param name="maxAttempts">The maximum number of times to attempt to run the test.</param>
|
||||
public static void ExecuteWithRetry(Action action, int maxAttempts = DefaultAttemptsForExpectedEvent)
|
||||
{
|
||||
for (int retry = 0; retry < maxAttempts; retry++)
|
||||
{
|
||||
try
|
||||
{
|
||||
action();
|
||||
return;
|
||||
}
|
||||
catch (Exception e) when (!(e is XunitException) && retry < maxAttempts - 1)
|
||||
{
|
||||
Thread.Sleep(RetryDelayMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does verification that the given watcher will not throw exactly/only the events in "expectedEvents" when
|
||||
/// "action" is executed.
|
||||
/// </summary>
|
||||
/// <param name="watcher">The FileSystemWatcher to test</param>
|
||||
/// <param name="unExpectedEvents">All of the events that are expected to be raised by this action</param>
|
||||
/// <param name="action">The Action that will trigger events.</param>
|
||||
/// <param name="cleanup">Optional. Undoes the action and cleans up the watcher so the test may be run again if necessary.</param>
|
||||
/// <param name="expectedPath">Optional. Adds path verification to all expected events.</param>
|
||||
public static void ExpectNoEvent(FileSystemWatcher watcher, WatcherChangeTypes unExpectedEvents, Action action, Action cleanup = null, string expectedPath = null, int timeout = WaitForExpectedEventTimeout)
|
||||
{
|
||||
bool result = ExecuteAndVerifyEvents(watcher, unExpectedEvents, action, false, new string[] { expectedPath }, timeout);
|
||||
Assert.False(result, "Expected Event occured");
|
||||
|
||||
if (cleanup != null)
|
||||
cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper for the ExpectEvent function.
|
||||
/// </summary>
|
||||
|
@ -199,8 +235,8 @@ namespace System.IO.Tests
|
|||
public static bool ExecuteAndVerifyEvents(FileSystemWatcher watcher, WatcherChangeTypes expectedEvents, Action action, bool assertExpected, string[] expectedPaths, int timeout)
|
||||
{
|
||||
bool result = true, verifyChanged = true, verifyCreated = true, verifyDeleted = true, verifyRenamed = true;
|
||||
AutoResetEvent changed = null, created = null, deleted = null, renamed = null;
|
||||
string[] expectedFullPaths = expectedPaths == null ? null : expectedPaths.Select(e => Path.GetFullPath(e)).ToArray();
|
||||
(AutoResetEvent EventOccured, FileSystemEventHandler Handler) changed = default, created = default, deleted = default;
|
||||
(AutoResetEvent EventOccured, RenamedEventHandler Handler) renamed = default;
|
||||
|
||||
if (verifyChanged = ((expectedEvents & WatcherChangeTypes.Changed) > 0))
|
||||
changed = WatchChanged(watcher, expectedPaths);
|
||||
|
@ -218,7 +254,8 @@ namespace System.IO.Tests
|
|||
if (verifyChanged)
|
||||
{
|
||||
bool Changed_expected = ((expectedEvents & WatcherChangeTypes.Changed) > 0);
|
||||
bool Changed_actual = changed.WaitOne(timeout);
|
||||
bool Changed_actual = changed.EventOccured.WaitOne(timeout);
|
||||
watcher.Changed -= changed.Handler;
|
||||
result = Changed_expected == Changed_actual;
|
||||
if (assertExpected)
|
||||
Assert.True(Changed_expected == Changed_actual, "Changed event did not occur as expected");
|
||||
|
@ -228,7 +265,8 @@ namespace System.IO.Tests
|
|||
if (verifyCreated)
|
||||
{
|
||||
bool Created_expected = ((expectedEvents & WatcherChangeTypes.Created) > 0);
|
||||
bool Created_actual = created.WaitOne(verifyChanged ? SubsequentExpectedWait : timeout);
|
||||
bool Created_actual = created.EventOccured.WaitOne(verifyChanged ? SubsequentExpectedWait : timeout);
|
||||
watcher.Created -= created.Handler;
|
||||
result = result && Created_expected == Created_actual;
|
||||
if (assertExpected)
|
||||
Assert.True(Created_expected == Created_actual, "Created event did not occur as expected");
|
||||
|
@ -238,7 +276,8 @@ namespace System.IO.Tests
|
|||
if (verifyDeleted)
|
||||
{
|
||||
bool Deleted_expected = ((expectedEvents & WatcherChangeTypes.Deleted) > 0);
|
||||
bool Deleted_actual = deleted.WaitOne(verifyChanged || verifyCreated ? SubsequentExpectedWait : timeout);
|
||||
bool Deleted_actual = deleted.EventOccured.WaitOne(verifyChanged || verifyCreated ? SubsequentExpectedWait : timeout);
|
||||
watcher.Deleted -= deleted.Handler;
|
||||
result = result && Deleted_expected == Deleted_actual;
|
||||
if (assertExpected)
|
||||
Assert.True(Deleted_expected == Deleted_actual, "Deleted event did not occur as expected");
|
||||
|
@ -248,7 +287,8 @@ namespace System.IO.Tests
|
|||
if (verifyRenamed)
|
||||
{
|
||||
bool Renamed_expected = ((expectedEvents & WatcherChangeTypes.Renamed) > 0);
|
||||
bool Renamed_actual = renamed.WaitOne(verifyChanged || verifyCreated || verifyDeleted? SubsequentExpectedWait : timeout);
|
||||
bool Renamed_actual = renamed.EventOccured.WaitOne(verifyChanged || verifyCreated || verifyDeleted ? SubsequentExpectedWait : timeout);
|
||||
watcher.Renamed -= renamed.Handler;
|
||||
result = result && Renamed_expected == Renamed_actual;
|
||||
if (assertExpected)
|
||||
Assert.True(Renamed_expected == Renamed_actual, "Renamed event did not occur as expected");
|
||||
|
@ -409,7 +449,7 @@ namespace System.IO.Tests
|
|||
foreach (NotifyFilters filter in Enum.GetValues(typeof(NotifyFilters)))
|
||||
yield return new object[] { filter };
|
||||
}
|
||||
|
||||
|
||||
// Linux and OSX systems have less precise filtering systems than Windows, so most
|
||||
// metadata filters are effectively equivalent to each other on those systems. For example
|
||||
// there isn't a way to filter only LastWrite events on either system; setting
|
||||
|
@ -430,5 +470,19 @@ namespace System.IO.Tests
|
|||
NotifyFilters.LastAccess |
|
||||
NotifyFilters.LastWrite |
|
||||
NotifyFilters.Size;
|
||||
|
||||
#if MONO
|
||||
private static FileSystemWatcher RecreateWatcher(FileSystemWatcher watcher)
|
||||
{
|
||||
FileSystemWatcher newWatcher = new FileSystemWatcher()
|
||||
{
|
||||
IncludeSubdirectories = watcher.IncludeSubdirectories,
|
||||
NotifyFilter = watcher.NotifyFilter,
|
||||
Path = watcher.Path,
|
||||
InternalBufferSize = watcher.InternalBufferSize
|
||||
};
|
||||
return newWatcher;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
|
@ -397,27 +397,31 @@ namespace System.IO
|
|||
}
|
||||
}
|
||||
|
||||
public static bool DirectoryExists(string fullPath)
|
||||
public static bool DirectoryExists(ReadOnlySpan<char> fullPath)
|
||||
{
|
||||
Interop.ErrorInfo ignored;
|
||||
return DirectoryExists(fullPath, out ignored);
|
||||
}
|
||||
|
||||
private static bool DirectoryExists(string fullPath, out Interop.ErrorInfo errorInfo)
|
||||
private static bool DirectoryExists(ReadOnlySpan<char> fullPath, out Interop.ErrorInfo errorInfo)
|
||||
{
|
||||
return FileExists(fullPath, Interop.Sys.FileTypes.S_IFDIR, out errorInfo);
|
||||
}
|
||||
|
||||
public static bool FileExists(string fullPath)
|
||||
public static bool FileExists(ReadOnlySpan<char> fullPath)
|
||||
{
|
||||
Interop.ErrorInfo ignored;
|
||||
|
||||
// Input allows trailing separators in order to match Windows behavior
|
||||
// Unix does not accept trailing separators, so must be trimmed
|
||||
Interop.ErrorInfo ignored;
|
||||
// File.Exists() explicitly checks for a trailing separator and returns false if found. FileInfo.Exists and all other
|
||||
// internal usages do not check for the trailing separator. Historically we've always removed the trailing separator
|
||||
// when getting attributes as trailing separators are generally not accepted by Windows APIs. Unix will take
|
||||
// trailing separators, but it infers that the path must be a directory (it effectively appends "."). To align with
|
||||
// our historical behavior (outside of File.Exists()), we need to trim.
|
||||
//
|
||||
// See http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11 for details.
|
||||
return FileExists(PathInternal.TrimEndingDirectorySeparator(fullPath), Interop.Sys.FileTypes.S_IFREG, out ignored);
|
||||
}
|
||||
|
||||
private static bool FileExists(string fullPath, int fileType, out Interop.ErrorInfo errorInfo)
|
||||
private static bool FileExists(ReadOnlySpan<char> fullPath, int fileType, out Interop.ErrorInfo errorInfo)
|
||||
{
|
||||
Debug.Assert(fileType == Interop.Sys.FileTypes.S_IFREG || fileType == Interop.Sys.FileTypes.S_IFDIR);
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче