This repository was archived by the owner on Jan 23, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Fix wrong behaviour when move multiple files out of watched folder on mac #41963
Merged
Merged
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
8fe0868
FS
d1fdf60
Multiple rename macOS fix
fa6de51
Multiple rename macOS fix
9d4caf7
Merge branch 'jahoda-filewatcher' of github.com:aik-jahoda/corefx int…
ce949f4
Format code
0307fc5
Clean proj file
039821b
Apply PR comments
4819da0
Merge branch 'master' into jahoda-filewatcher
b3f9460
Apply suggestions from code review
aik-jahoda 70fec6d
Update src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatch…
aik-jahoda 610aaf1
Apply PR comments
81a3b44
Apply PR comments
4250e56
Apply PR comments
deb8f7c
Document rename event id behaviour
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -88,9 +88,9 @@ private static FSEventStreamEventFlags TranslateFlags(NotifyFilters flagsToTrans | |
// Always re-create the filter flags when start is called since they could have changed | ||
if ((flagsToTranslate & (NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Size)) != 0) | ||
{ | ||
flags = FSEventStreamEventFlags.kFSEventStreamEventFlagItemInodeMetaMod | | ||
flags = FSEventStreamEventFlags.kFSEventStreamEventFlagItemInodeMetaMod | | ||
FSEventStreamEventFlags.kFSEventStreamEventFlagItemFinderInfoMod | | ||
FSEventStreamEventFlags.kFSEventStreamEventFlagItemModified | | ||
FSEventStreamEventFlags.kFSEventStreamEventFlagItemModified | | ||
FSEventStreamEventFlags.kFSEventStreamEventFlagItemChangeOwner; | ||
} | ||
if ((flagsToTranslate & NotifyFilters.Security) != 0) | ||
|
@@ -122,7 +122,7 @@ private sealed class RunningInstance | |
{ | ||
// Flags used to create the event stream | ||
private const Interop.EventStream.FSEventStreamCreateFlags EventStreamFlags = (Interop.EventStream.FSEventStreamCreateFlags.kFSEventStreamCreateFlagFileEvents | | ||
Interop.EventStream.FSEventStreamCreateFlags.kFSEventStreamCreateFlagNoDefer | | ||
Interop.EventStream.FSEventStreamCreateFlags.kFSEventStreamCreateFlagNoDefer | | ||
Interop.EventStream.FSEventStreamCreateFlags.kFSEventStreamCreateFlagWatchRoot); | ||
|
||
// Weak reference to the associated watcher. A weak reference is used so that the FileSystemWatcher may be collected and finalized, | ||
|
@@ -371,33 +371,33 @@ private unsafe void FileSystemEventCallback( | |
if (context is null) | ||
{ | ||
// Flow suppressed, just run here | ||
ProcessEvents(numEvents.ToInt32(), eventPaths, eventFlags, eventIds, watcher); | ||
ProcessEvents(numEvents.ToInt32(), eventPaths, new Span<FSEventStreamEventFlags>(eventFlags, numEvents.ToInt32()), new Span<FSEventStreamEventId>(eventIds, numEvents.ToInt32()), watcher); | ||
} | ||
else | ||
{ | ||
ExecutionContext.Run( | ||
context, | ||
(object o) => ((RunningInstance)o).ProcessEvents(numEvents.ToInt32(), eventPaths, eventFlags, eventIds, watcher), | ||
(object o) => ((RunningInstance)o).ProcessEvents(numEvents.ToInt32(), eventPaths, new Span<FSEventStreamEventFlags>(eventFlags, numEvents.ToInt32()), new Span<FSEventStreamEventId>(eventIds, numEvents.ToInt32()), watcher), | ||
aik-jahoda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
this); | ||
} | ||
} | ||
|
||
private unsafe void ProcessEvents(int numEvents, | ||
byte** eventPaths, | ||
FSEventStreamEventFlags* eventFlags, | ||
FSEventStreamEventId* eventIds, | ||
Span<FSEventStreamEventFlags> eventFlags, | ||
Span<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(); | ||
// Since renames come in pairs, when we reach the first we need to test for the next one if it is the case. If the next one belongs into the pair, | ||
// we'll store the event id 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. | ||
int? handledRenameEvents = null; | ||
stephentoub marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
for (int i = 0; i < numEvents; i++) | ||
{ | ||
ReadOnlySpan<char> path = events[i].Span; | ||
Debug.Assert(path[path.Length - 1] != '/', "Trailing slashes on events is not supported"); | ||
using ParsedEvent parsedEvent = ParseEvent(eventPaths[i]); | ||
|
||
ReadOnlySpan<char> path = parsedEvent.Path; | ||
Debug.Assert(path[^1] != '/', "Trailing slashes on events is not supported"); | ||
|
||
// Match Windows and don't notify us about changes to the Root folder | ||
if (_fullDirectory.Length >= path.Length && path.Equals(_fullDirectory.AsSpan(0, path.Length), StringComparison.OrdinalIgnoreCase)) | ||
|
@@ -412,7 +412,7 @@ private unsafe void ProcessEvents(int numEvents, | |
watcher.OnError(new ErrorEventArgs(new IOException(SR.FSW_BufferOverflow, (int)eventFlags[i]))); | ||
break; | ||
} | ||
else if ((handledRenameEvents != null) && (handledRenameEvents.Contains(eventIds[i]))) | ||
else if (handledRenameEvents == i) | ||
{ | ||
// If this event is the second in a rename pair then skip it | ||
continue; | ||
|
@@ -443,15 +443,15 @@ private unsafe void ProcessEvents(int numEvents, | |
} | ||
if (((eventType & WatcherChangeTypes.Renamed) > 0)) | ||
{ | ||
// Find the rename that is paired to this rename, which should be the next rename in the list | ||
int pairedId = FindRenameChangePairedChange(i, eventFlags, numEvents); | ||
if (pairedId == int.MinValue) | ||
// Find the rename that is paired to this rename. | ||
int? pairedId = FindRenameChangePairedChange(i, eventFlags, eventIds); | ||
if (!pairedId.HasValue) | ||
{ | ||
// Getting here means we have a rename without a pair, meaning it should be a create for the | ||
// move from unwatched folder to watcher folder scenario or a move from the watcher folder out. | ||
// Check if the item exists on disk to check which it is | ||
// Don't send a new notification if we already sent one for this event. | ||
if (DoesItemExist(path, IsFlagSet(eventFlags[i], FSEventStreamEventFlags.kFSEventStreamEventFlagItemIsFile))) | ||
if (DoesItemExist(path, eventFlags[i].HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagItemIsFile))) | ||
{ | ||
if ((eventType & WatcherChangeTypes.Created) == 0) | ||
{ | ||
|
@@ -467,55 +467,63 @@ private unsafe void ProcessEvents(int numEvents, | |
{ | ||
// Remove the base directory prefix and add the paired event to the list of | ||
// events to skip and notify the user of the rename | ||
ReadOnlySpan<char> newPathRelativeName = events[pairedId].Span; | ||
if (newPathRelativeName.Length >= _fullDirectory.Length && | ||
newPathRelativeName.StartsWith(_fullDirectory, StringComparison.OrdinalIgnoreCase)) | ||
using (ParsedEvent pairedEvent = ParseEvent(eventPaths[pairedId.GetValueOrDefault()])) | ||
{ | ||
newPathRelativeName = newPathRelativeName.Slice(_fullDirectory.Length); | ||
} | ||
watcher.NotifyRenameEventArgs(WatcherChangeTypes.Renamed, newPathRelativeName, relativePath); | ||
ReadOnlySpan<char> newPathRelativeName = pairedEvent.Path; | ||
if (newPathRelativeName.Length >= _fullDirectory.Length && | ||
newPathRelativeName.StartsWith(_fullDirectory, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
newPathRelativeName = newPathRelativeName.Slice(_fullDirectory.Length); | ||
} | ||
|
||
// Create a new list, if necessary, and add the event | ||
if (handledRenameEvents == null) | ||
{ | ||
handledRenameEvents = new List<FSEventStreamEventId>(); | ||
watcher.NotifyRenameEventArgs(WatcherChangeTypes.Renamed, newPathRelativeName, relativePath); | ||
} | ||
handledRenameEvents.Add(eventIds[pairedId]); | ||
handledRenameEvents = pairedId.GetValueOrDefault(); | ||
} | ||
} | ||
} | ||
|
||
ArraySegment<char> underlyingArray; | ||
if (MemoryMarshal.TryGetArray(events[i], out underlyingArray)) | ||
ArrayPool<char>.Shared.Return(underlyingArray.Array); | ||
} | ||
|
||
this._context = ExecutionContext.Capture(); | ||
|
||
void ParseEvents() | ||
ParsedEvent ParseEvent(byte* nativeEventPath) | ||
{ | ||
for (int i = 0; i < events.Length; i++) | ||
int byteCount = 0; | ||
Debug.Assert(nativeEventPath != null); | ||
byte* temp = nativeEventPath; | ||
|
||
// Finds the position of null character. | ||
while (*temp != 0) | ||
{ | ||
int byteCount = 0; | ||
Debug.Assert(eventPaths[i] != null); | ||
byte* temp = eventPaths[i]; | ||
temp++; | ||
byteCount++; | ||
} | ||
|
||
// Finds the position of null character. | ||
while (*temp != 0) | ||
{ | ||
temp++; | ||
byteCount++; | ||
} | ||
Debug.Assert(byteCount > 0, "Empty events are not supported"); | ||
char[] tempBuffer = ArrayPool<char>.Shared.Rent(Encoding.UTF8.GetMaxCharCount(byteCount)); | ||
|
||
// Converting an array of bytes to UTF-8 char array | ||
int charCount = Encoding.UTF8.GetChars(new ReadOnlySpan<byte>(nativeEventPath, byteCount), tempBuffer); | ||
return new ParsedEvent(tempBuffer.AsSpan(0, charCount), tempBuffer); | ||
} | ||
|
||
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); | ||
} | ||
private readonly ref struct ParsedEvent | ||
{ | ||
|
||
public ParsedEvent(ReadOnlySpan<char> path, char[] tempBuffer) | ||
{ | ||
TempBuffer = tempBuffer; | ||
Path = path; | ||
} | ||
|
||
public readonly ReadOnlySpan<char> Path; | ||
|
||
public readonly char[] TempBuffer; | ||
aik-jahoda marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public void Dispose() => ArrayPool<char>.Shared.Return(TempBuffer); | ||
|
||
} | ||
|
||
/// <summary> | ||
|
@@ -547,15 +555,15 @@ private WatcherChangeTypes FilterEvents(FSEventStreamEventFlags eventFlags) | |
if (eventIsCorrectType || ((allowDirs || allowFiles) && (eventIsLink))) | ||
{ | ||
// Notify Created/Deleted/Renamed events. | ||
if (IsFlagSet(eventFlags, FSEventStreamEventFlags.kFSEventStreamEventFlagItemRenamed)) | ||
if (eventFlags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagItemRenamed)) | ||
{ | ||
eventType |= WatcherChangeTypes.Renamed; | ||
} | ||
if (IsFlagSet(eventFlags, FSEventStreamEventFlags.kFSEventStreamEventFlagItemCreated)) | ||
if (eventFlags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagItemCreated)) | ||
{ | ||
eventType |= WatcherChangeTypes.Created; | ||
} | ||
if (IsFlagSet(eventFlags, FSEventStreamEventFlags.kFSEventStreamEventFlagItemRemoved)) | ||
if (eventFlags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagItemRemoved)) | ||
{ | ||
eventType |= WatcherChangeTypes.Deleted; | ||
} | ||
|
@@ -566,12 +574,12 @@ private WatcherChangeTypes FilterEvents(FSEventStreamEventFlags eventFlags) | |
private bool ShouldRescanOccur(FSEventStreamEventFlags flags) | ||
{ | ||
// Check if any bit is set that signals that the caller should rescan | ||
return (IsFlagSet(flags, FSEventStreamEventFlags.kFSEventStreamEventFlagMustScanSubDirs) || | ||
IsFlagSet(flags, FSEventStreamEventFlags.kFSEventStreamEventFlagUserDropped) || | ||
IsFlagSet(flags, FSEventStreamEventFlags.kFSEventStreamEventFlagKernelDropped) || | ||
IsFlagSet(flags, FSEventStreamEventFlags.kFSEventStreamEventFlagRootChanged) || | ||
IsFlagSet(flags, FSEventStreamEventFlags.kFSEventStreamEventFlagMount) || | ||
IsFlagSet(flags, FSEventStreamEventFlags.kFSEventStreamEventFlagUnmount)); | ||
return (flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagMustScanSubDirs) || | ||
flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagUserDropped) || | ||
flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagKernelDropped) || | ||
flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagRootChanged) || | ||
flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagMount) || | ||
flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagUnmount)); | ||
} | ||
|
||
private bool CheckIfPathIsNested(ReadOnlySpan<char> eventPath) | ||
|
@@ -582,36 +590,36 @@ private bool CheckIfPathIsNested(ReadOnlySpan<char> eventPath) | |
return _includeChildren || _fullDirectory.AsSpan().StartsWith(System.IO.Path.GetDirectoryName(eventPath), StringComparison.OrdinalIgnoreCase); | ||
} | ||
|
||
private unsafe int FindRenameChangePairedChange( | ||
private unsafe int? FindRenameChangePairedChange( | ||
int currentIndex, | ||
FSEventStreamEventFlags* eventFlags, | ||
int numEvents) | ||
Span<FSEventStreamEventFlags> flags, Span<FSEventStreamEventId> ids) | ||
{ | ||
// Start at one past the current index and try to find the next Rename item, which should be the old path. | ||
for (int i = currentIndex + 1; i < numEvents; i++) | ||
// The rename event can be composed of two events. The first contains the original file name the second contains the new file name. | ||
// Each of the events is delivered only when the corresponding folder is watched. It means both events are delivered when the rename/move | ||
// occurs inside the watched folder. When the move has origin o final destination outside, only one event is delivered. To distinguish | ||
// between two nonrelated events and the event which belong together the event ID is tested. Only related rename events differ in ID by one. | ||
// This behavior isn't documented and there is an open radar http://www.openradar.me/13461247. | ||
|
||
int nextIndex = currentIndex + 1; | ||
|
||
if (nextIndex >= flags.Length) | ||
return null; | ||
|
||
if (ids[currentIndex] + 1 == ids[nextIndex] && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I would add comment explaining current assumption and reasoning for it. |
||
flags[nextIndex].HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagItemRenamed)) | ||
{ | ||
if (IsFlagSet(eventFlags[i], FSEventStreamEventFlags.kFSEventStreamEventFlagItemRenamed)) | ||
{ | ||
// We found match, stop looking | ||
return i; | ||
} | ||
return nextIndex; | ||
} | ||
|
||
return int.MinValue; | ||
return null; | ||
} | ||
|
||
private static bool IsFlagSet(FSEventStreamEventFlags flags, FSEventStreamEventFlags value) | ||
{ | ||
return (value & flags) == value; | ||
} | ||
|
||
private static bool DoesItemExist(ReadOnlySpan<char> path, bool isFile) | ||
{ | ||
if (path.IsEmpty || path.Length == 0) | ||
return false; | ||
|
||
if (!isFile) | ||
return FileSystem.DirectoryExists(path); | ||
return FileSystem.DirectoryExists(path); | ||
|
||
return PathInternal.IsDirectorySeparator(path[path.Length - 1]) | ||
? false | ||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.