Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit c47d44a

Browse files
committed
Merge pull request #2587 from sokket/fsw
Adding new Unit Tests to validate the more complex FileSystemWatcher scenarios
2 parents dba869e + c41c161 commit c47d44a

File tree

12 files changed

+559
-20
lines changed

12 files changed

+559
-20
lines changed

src/Common/src/Interop/Linux/libc/Interop.inotify.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ internal enum NotifyEvents
5353
IN_Q_OVERFLOW = 0x00004000,
5454
IN_IGNORED = 0x00008000,
5555
IN_ONLYDIR = 0x01000000,
56+
IN_DONT_FOLLOW = 0x02000000,
5657
IN_EXCL_UNLINK = 0x04000000,
5758
IN_ISDIR = 0x40000000,
5859
}

src/Common/src/Interop/Unix/System.IO.Native/Interop.NativeIO.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,8 @@ internal enum FileStatsFlags
4343

4444
[DllImport(Libraries.IOInterop, SetLastError = true)]
4545
internal static extern int Stat(string path, out FileStats output);
46+
47+
[DllImport(Libraries.IOInterop, SetLastError = true)]
48+
internal static extern int LStat(string path, out FileStats output);
4649
}
4750
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
using System.Runtime.InteropServices;
6+
7+
internal static partial class Interop
8+
{
9+
internal static partial class libc
10+
{
11+
[Flags]
12+
internal enum PollFlags : short
13+
{
14+
POLLIN = 0x0001, /* any readable data available */
15+
POLLERR = 0x0008, /* some poll error occurred */
16+
POLLHUP = 0x0010, /* file descriptor was "hung up" */
17+
POLLNVAL = 0x0020, /* requested events "invalid" */
18+
}
19+
20+
[StructLayout(LayoutKind.Sequential)]
21+
internal struct pollfd
22+
{
23+
internal int fd; /* file descriptor to poll*/
24+
internal PollFlags events; /* events to poll for */
25+
internal PollFlags revents; /* events received from polling */
26+
}
27+
28+
/// <summary>
29+
/// Polls a set of file descriptors for signals and returns what signals have been set
30+
/// </summary>
31+
/// <param name="fds">A pointer to pollfd structs to look for</param>
32+
/// <param name="count">The number of entries in fds</param>
33+
/// <param name="timeout">The amount of time to wait; -1 for infinite, 0 for immediate return, and a positive number is the number of milliseconds</param>
34+
/// <returns>
35+
/// Returns a positive number (which is the number of structures with nonzero revent files), 0 for a timeout or no
36+
/// descriptors were ready, or -1 on error.
37+
/// </returns>
38+
[DllImport(Libraries.Libc, SetLastError = true)]
39+
private static unsafe extern int poll(pollfd* fds, uint count, int timeout);
40+
41+
/// <summary>
42+
/// Polls a File Descriptor for the passed in flags.
43+
/// </summary>
44+
/// <param name="fd">The descriptor to poll</param>
45+
/// <param name="flags">The flags to poll for</param>
46+
/// <param name="timeout">The amount of time to wait; -1 for infinite, 0 for immediate return, and a positive number is the number of milliseconds</param>
47+
/// <param name="resultFlags">The flags that were returned by the poll call</param>
48+
/// <returns>
49+
/// Returns a positive number (which is the number of structures with nonzero revent files), 0 for a timeout or no
50+
/// descriptors were ready, or -1 on error.
51+
/// </returns>
52+
internal unsafe static int poll(int fd, PollFlags flags, int timeout, out PollFlags resultFlags)
53+
{
54+
pollfd pfd = default(pollfd);
55+
pfd.fd = fd;
56+
pfd.events = flags;
57+
int result = poll(&pfd, 1, timeout);
58+
resultFlags = pfd.revents;
59+
return result;
60+
}
61+
}
62+
}

src/Native/System.IO.Native/nativeio.cpp

100755100644
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
#if HAVE_STAT64
1111
# define stat_ stat64
1212
# define fstat_ fstat64
13+
# define lstat_ lstat64
1314
#else
1415
# define stat_ stat
1516
# define fstat_ fstat
17+
# define lstat_ lstat
1618
#endif
1719

1820
static void ConvertFileStats(const struct stat_& src, FileStats* dst)
@@ -59,4 +61,17 @@ extern "C"
5961

6062
return ret; // TODO: errno conversion
6163
}
64+
65+
int32_t LStat(const char* path, struct FileStats* output)
66+
{
67+
struct stat_ result;
68+
int ret = lstat_(path, &result);
69+
70+
if (ret == 0)
71+
{
72+
ConvertFileStats(result, output);
73+
}
74+
75+
return ret; // TODO: errno conversion
76+
}
6277
}

src/Native/System.IO.Native/nativeio.h

100755100644
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,12 @@ extern "C"
4141
* Returns 0 for success, -1 for failure. Sets errno on failure.
4242
*/
4343
int32_t Stat(const char* path, FileStats* output);
44+
45+
/**
46+
* Get file stats from a full path. Implemented as shim to lstat(2).
47+
*
48+
* Returns 0 for success, -1 for failure. Sets errno on failure.
49+
*/
50+
int32_t LStat(const char* path, FileStats* output);
4451
}
4552

src/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@
6666
<Compile Include="$(CommonPath)\Interop\Linux\Interop.Errors.cs">
6767
<Link>Common\Interop\Linux\Interop.Errors.cs</Link>
6868
</Compile>
69+
<Compile Include="$(CommonPath)\Interop\Unix\libc\Interop.poll.cs">
70+
<Link>Common\Interop\Unix\Interop.poll.cs</Link>
71+
</Compile>
72+
<Compile Include="$(CommonPath)\Interop\Unix\System.IO.Native\Interop.NativeIO.cs">
73+
<Link>Common\Interop\Unix\Interop.NativeIO.cs</Link>
74+
</Compile>
6975
</ItemGroup>
7076
<ItemGroup Condition=" '$(TargetsOSX)' == 'true' ">
7177
<Compile Include="System\IO\FileSystemWatcher.OSX.cs" />

src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -299,10 +299,20 @@ private WatchedDirectory AddDirectoryWatchUnlocked(WatchedDirectory parent, stri
299299
{
300300
string fullPath = parent != null ? parent.GetPath(false, directoryName) : directoryName;
301301

302+
// inotify_add_watch will fail if this is a symlink, so check that we didn't get a symlink
303+
Interop.NativeIO.FileStats stats = default(Interop.NativeIO.FileStats);
304+
if ((Interop.NativeIO.LStat(fullPath, out stats) == 0) &&
305+
((stats.Mode & (uint)Interop.NativeIO.FileTypes.S_IFMT) == Interop.NativeIO.FileTypes.S_IFLNK))
306+
{
307+
return null;
308+
}
309+
302310
// Add a watch for the full path. If the path is already being watched, this will return
303-
// the existing descriptor. This works even in the case of a rename.
311+
// the existing descriptor. This works even in the case of a rename. We also add the DONT_FOLLOW
312+
// and EXCL_UNLINK flags to keep parity with Windows where we don't pickup symlinks or unlinked
313+
// files (which don't exist in Windows)
304314
int wd = (int)SysCall(
305-
(fd, path, thisRef) => Interop.libc.inotify_add_watch(fd, path, (uint)thisRef._notifyFilters),
315+
(fd, path, thisRef) => Interop.libc.inotify_add_watch(fd, path, (uint)(thisRef._notifyFilters | Interop.libc.NotifyEvents.IN_DONT_FOLLOW | Interop.libc.NotifyEvents.IN_EXCL_UNLINK)),
306316
fullPath,
307317
this);
308318

@@ -476,7 +486,21 @@ private void ProcessEvents()
476486
string expandedName = null;
477487
WatchedDirectory associatedDirectoryEntry = null;
478488

479-
if (nextEvent.wd != -1) // wd is -1 for events like IN_Q_OVERFLOW that aren't tied to a particular watch descriptor
489+
// An overflow event means that we can't trust our state without restarting since we missed events and
490+
// some of those events could be a directory create, meaning we wouldn't have added the directory to the
491+
// watch and would not provide correct data to the caller.
492+
if ((mask & (uint)Interop.libc.NotifyEvents.IN_Q_OVERFLOW) != 0)
493+
{
494+
// Notify the caller of the error and, if the includeSubdirectories flag is set, restart to pick up any
495+
// potential directories we missed due to the overflow.
496+
watcher.NotifyInternalBufferOverflowEvent();
497+
if (_includeSubdirectories)
498+
{
499+
watcher.Restart();
500+
}
501+
break;
502+
}
503+
else
480504
{
481505
// Look up the directory information for the supplied wd
482506
lock (SyncObj)
@@ -543,15 +567,11 @@ private void ProcessEvents()
543567
}
544568

545569
const Interop.libc.NotifyEvents switchMask =
546-
Interop.libc.NotifyEvents.IN_Q_OVERFLOW | Interop.libc.NotifyEvents.IN_IGNORED |
547-
Interop.libc.NotifyEvents.IN_CREATE | Interop.libc.NotifyEvents.IN_DELETE |
570+
Interop.libc.NotifyEvents.IN_IGNORED |Interop.libc.NotifyEvents.IN_CREATE | Interop.libc.NotifyEvents.IN_DELETE |
548571
Interop.libc.NotifyEvents.IN_ACCESS | Interop.libc.NotifyEvents.IN_MODIFY | Interop.libc.NotifyEvents.IN_ATTRIB |
549572
Interop.libc.NotifyEvents.IN_MOVED_FROM | Interop.libc.NotifyEvents.IN_MOVED_TO;
550573
switch ((Interop.libc.NotifyEvents)(mask & (uint)switchMask))
551574
{
552-
case Interop.libc.NotifyEvents.IN_Q_OVERFLOW:
553-
watcher.NotifyInternalBufferOverflowEvent();
554-
break;
555575
case Interop.libc.NotifyEvents.IN_CREATE:
556576
watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Created, expandedName);
557577
break;
@@ -573,9 +593,35 @@ private void ProcessEvents()
573593
watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Changed, expandedName);
574594
break;
575595
case Interop.libc.NotifyEvents.IN_MOVED_FROM:
596+
// We need to check if this MOVED_FROM event is standalone - meaning the item was moved out
597+
// of scope. We do this by checking if we are at the end of our buffer (meaning no more events)
598+
// and if there is data to be read by polling the fd. If there aren't any more events, fire the
599+
// deleted event; if there are more events, handle it via next pass. This adds an additional
600+
// edge case where we get the MOVED_FROM event and the MOVED_TO event hasn't been generated yet
601+
// so we will send a DELETE for this event and a CREATE when the MOVED_TO is eventually processed.
602+
if (_bufferPos == _bufferAvailable)
603+
{
604+
bool success = false;
605+
Interop.libc.PollFlags resultFlags;
606+
_inotifyHandle.DangerousAddRef(ref success);
607+
Debug.Assert(success, "Failed to add-ref inotify handle");
608+
int result = Interop.libc.poll(_inotifyHandle.DangerousGetHandle().ToInt32(), Interop.libc.PollFlags.POLLIN, 0, out resultFlags);
609+
_inotifyHandle.DangerousRelease();
610+
611+
// If we error or don't have any signaled handles, send the deleted event
612+
if (result <= 0)
613+
{
614+
// There isn't any more data in the queue so this is a deleted event
615+
watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, expandedName);
616+
break;
617+
}
618+
}
619+
620+
// We will set these values if the buffer has more data OR if the poll call tells us that more data is available.
576621
previousEventName = expandedName;
577622
previousEventParent = isDir ? associatedDirectoryEntry : null;
578623
previousEventCookie = nextEvent.cookie;
624+
579625
break;
580626
case Interop.libc.NotifyEvents.IN_MOVED_TO:
581627
if (previousEventName != null)

src/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ private void FileSystemEventCallback(
229229
FSEventStreamEventId[] eventIds)
230230
{
231231
Debug.Assert((numEvents.ToInt32() == eventPaths.Length) && (numEvents.ToInt32() == eventFlags.Length) && (numEvents.ToInt32() == eventIds.Length));
232-
232+
233233
// 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
234234
// 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.
235235
List<FSEventStreamEventId> handledRenameEvents = null;
@@ -310,19 +310,21 @@ private void FileSystemEventCallback(
310310
}
311311
else
312312
{
313-
// Note: we keep the same order (Create, delete, modify) as *nix but since OS X does some coalescing on it's own, we need to
314-
// fire multiple events for the same item (potentially).
315-
if (IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemCreated))
316-
{
317-
// Next look for creates since a create + modification coalesced could confuse apps since a file they haven't heard of yet would get a mod event
318-
NotifyFileSystemEventArgs(WatcherChangeTypes.Created, relativePath);
319-
}
320-
321-
if (IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemRemoved))
313+
// OS X is wonky where it can give back kFSEventStreamEventFlagItemCreated and kFSEventStreamEventFlagItemRemoved
314+
// for the same item. The only option we have is to stat and see if the item exists; if so send created, otherwise send deleted.
315+
if ((IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemCreated)) ||
316+
(IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemRemoved)))
322317
{
323-
NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, relativePath);
318+
if (DoesItemExist(eventPaths[i], IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemIsFile)))
319+
{
320+
NotifyFileSystemEventArgs(WatcherChangeTypes.Created, relativePath);
321+
}
322+
else
323+
{
324+
NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, relativePath);
325+
}
324326
}
325-
327+
326328
if (IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemInodeMetaMod) ||
327329
IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemModified) ||
328330
IsFlagSet(eventFlags[i], Interop.EventStream.FSEventStreamEventFlags.kFSEventStreamEventFlagItemFinderInfoMod) ||

0 commit comments

Comments
 (0)