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:
Maxim Lipnin 2019-01-11 14:01:31 +03:00 коммит произвёл Marek Safar
Родитель 1c7e47f64b
Коммит 8d1b6abd3e
10 изменённых файлов: 908 добавлений и 250 удалений

Просмотреть файл

@ -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);