private void ProcessEvents()
{
// When cancellation is requested, clear out all watches. This should force any active or future reads
// on the inotify handle to return 0 bytes read immediately, allowing us to wake up from the blocking call
// and exit the processing loop and clean up.
var ctr = _cancellationToken.Register(obj => ((RunningInstance)obj).CancellationCallback(), this);
try
{
// Previous event information
string previousEventName = null;
WatchedDirectory previousEventParent = null;
uint previousEventCookie = 0;
// Process events as long as we're not canceled and there are more to read...
NotifyEvent nextEvent;
while (!_cancellationToken.IsCancellationRequested && TryReadEvent(out nextEvent))
{
// 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
// if the watcher is dropped by the user without being disposed. If we can't get the watcher,
// there's nothing more to do (we can't raise events), so bail.
FileSystemWatcher watcher;
if (!_weakWatcher.TryGetTarget(out watcher))
{
break;
}
uint mask = nextEvent.mask;
string expandedName = null;
WatchedDirectory associatedDirectoryEntry = null;
// An overflow event means that we can't trust our state without restarting since we missed events and
// some of those events could be a directory create, meaning we wouldn't have added the directory to the
// watch and would not provide correct data to the caller.
if ((mask & (uint)Interop.Sys.NotifyEvents.IN_Q_OVERFLOW) != 0)
{
// Notify the caller of the error and, if the includeSubdirectories flag is set, restart to pick up any
// potential directories we missed due to the overflow.
watcher.NotifyInternalBufferOverflowEvent();
if (_includeSubdirectories)
{
watcher.Restart();
}
break;
}
else
{
// Look up the directory information for the supplied wd
lock (SyncObj)
{
if (!_wdToPathMap.TryGetValue(nextEvent.wd, out associatedDirectoryEntry))
{
// The watch descriptor could be missing from our dictionary if it was removed
// due to cancellation, or if we already removed it and this is a related event
// like IN_IGNORED. In any case, just ignore it... even if for some reason we
// should have the value, there's little we can do about it at this point,
// and there's no more processing of this event we can do without it.
continue;
}
}
expandedName = associatedDirectoryEntry.GetPath(true, nextEvent.name);
}
// To match Windows, ignore all changes that happen on the root folder itself
if (string.IsNullOrEmpty(expandedName))
{
watcher = null;
continue;
}
// Determine whether the affected object is a directory (rather than a file).
// If it is, we may need to do special processing, such as adding a watch for new
// directories if IncludeSubdirectories is enabled. Since we're only watching
// directories, any IN_IGNORED event is also for a directory.
bool isDir = (mask & (uint)(Interop.Sys.NotifyEvents.IN_ISDIR | Interop.Sys.NotifyEvents.IN_IGNORED)) != 0;
// Renames come in the form of two events: IN_MOVED_FROM and IN_MOVED_TO.
// In general, these should come as a sequence, one immediately after the other.
// So, we delay raising an event for IN_MOVED_FROM until we see what comes next.
if (previousEventName != null && ((mask & (uint)Interop.Sys.NotifyEvents.IN_MOVED_TO) == 0 || previousEventCookie != nextEvent.cookie))
{
// IN_MOVED_FROM without an immediately-following corresponding IN_MOVED_TO.
// We have to assume that it was moved outside of our root watch path, which
// should be considered a deletion to match Win32 behavior.
// But since we explicitly added watches on directories, if it's a directory it'll
// still be watched, so we need to explicitly remove the watch.
if (previousEventParent != null && previousEventParent.Children != null)
{
// previousEventParent will be non-null iff the IN_MOVED_FROM
// was for a directory, in which case previousEventParent is that directory's
// parent and previousEventName is the name of the directory to be removed.
foreach (WatchedDirectory child in previousEventParent.Children)
{
if (child.Name == previousEventName)
{
RemoveWatchedDirectory(child);
break;
}
}
}
// Then fire the deletion event, even though the event was IN_MOVED_FROM.
watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, previousEventName);
previousEventName = null;
previousEventParent = null;
previousEventCookie = 0;
}
// If the event signaled that there's a new subdirectory and if we're monitoring subdirectories,
// add a watch for it.
const Interop.Sys.NotifyEvents AddMaskFilters = Interop.Sys.NotifyEvents.IN_CREATE | Interop.Sys.NotifyEvents.IN_MOVED_TO;
bool addWatch = ((mask & (uint)AddMaskFilters) != 0);
if (addWatch && isDir && _includeSubdirectories)
{
AddDirectoryWatch(associatedDirectoryEntry, nextEvent.name);
}
const Interop.Sys.NotifyEvents switchMask =
Interop.Sys.NotifyEvents.IN_IGNORED |Interop.Sys.NotifyEvents.IN_CREATE | Interop.Sys.NotifyEvents.IN_DELETE |
Interop.Sys.NotifyEvents.IN_ACCESS | Interop.Sys.NotifyEvents.IN_MODIFY | Interop.Sys.NotifyEvents.IN_ATTRIB |
Interop.Sys.NotifyEvents.IN_MOVED_FROM | Interop.Sys.NotifyEvents.IN_MOVED_TO;
switch ((Interop.Sys.NotifyEvents)(mask & (uint)switchMask))
{
case Interop.Sys.NotifyEvents.IN_CREATE:
watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Created, expandedName);
break;
case Interop.Sys.NotifyEvents.IN_IGNORED:
// We're getting an IN_IGNORED because a directory watch was removed.
// and we're getting this far in our code because we still have an entry for it
// in our dictionary. So we want to clean up the relevant state, but not clean
// attempt to call back to inotify to remove the watches.
RemoveWatchedDirectory(associatedDirectoryEntry, removeInotify:false);
break;
case Interop.Sys.NotifyEvents.IN_DELETE:
watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, expandedName);
// We don't explicitly RemoveWatchedDirectory here, as that'll be handled
// by IN_IGNORED processing if this is a directory.
break;
case Interop.Sys.NotifyEvents.IN_ACCESS:
case Interop.Sys.NotifyEvents.IN_MODIFY:
case Interop.Sys.NotifyEvents.IN_ATTRIB:
watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Changed, expandedName);
break;
case Interop.Sys.NotifyEvents.IN_MOVED_FROM:
// We need to check if this MOVED_FROM event is standalone - meaning the item was moved out
// of scope. We do this by checking if we are at the end of our buffer (meaning no more events)
// and if there is data to be read by polling the fd. If there aren't any more events, fire the
// deleted event; if there are more events, handle it via next pass. This adds an additional
// edge case where we get the MOVED_FROM event and the MOVED_TO event hasn't been generated yet
// so we will send a DELETE for this event and a CREATE when the MOVED_TO is eventually processed.
if (_bufferPos == _bufferAvailable)
{
// Do the poll with a small timeout value. Community research showed that a few milliseconds
// was enough to allow the vast majority of MOVED_TO events that were going to show
// up to actually arrive. This doesn't need to be perfect; there's always the chance
// that a MOVED_TO could show up after whatever timeout is specified, in which case
// it'll just result in a delete + create instead of a rename. We need the value to be
// small so that we don't significantly delay the delivery of the deleted event in case
// that's actually what's needed (otherwise it'd be fine to block indefinitely waiting
// for the next event to arrive).
const int MillisecondsTimeout = 2;
Interop.Sys.PollEvents events;
Interop.Sys.Poll(_inotifyHandle, Interop.Sys.PollEvents.POLLIN, MillisecondsTimeout, out events);
// If we error or don't have any signaled handles, send the deleted event
if (events == Interop.Sys.PollEvents.POLLNONE)
{
// There isn't any more data in the queue so this is a deleted event
watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, expandedName);
break;
}
}
// We will set these values if the buffer has more data OR if the poll call tells us that more data is available.
previousEventName = expandedName;
previousEventParent = isDir ? associatedDirectoryEntry : null;
previousEventCookie = nextEvent.cookie;
break;
case Interop.Sys.NotifyEvents.IN_MOVED_TO:
if (previousEventName != null)
{
// If the previous name from IN_MOVED_FROM is non-null, then this is a rename.
watcher.NotifyRenameEventArgs(WatcherChangeTypes.Renamed, expandedName, previousEventName);
}
else
{
// If it is null, then we didn't get an IN_MOVED_FROM (or we got it a long time
// ago and treated it as a deletion), in which case this is considered a creation.
watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Created, expandedName);
}
previousEventName = null;
previousEventParent = null;
previousEventCookie = 0;
break;
}
// Drop our strong reference to the watcher now that we're potentially going to block again for another read
watcher = null;
}
}
catch (Exception exc)
{
FileSystemWatcher watcher;
if (_weakWatcher.TryGetTarget(out watcher))
{
watcher.OnError(new ErrorEventArgs(exc));
}
}
finally
{
ctr.Dispose();
_inotifyHandle.Dispose();
}
}