private const int NAME_MAX = 255; // from limits.h /// <summary>Initializes the instance with all state necessary to operate a watch.</summary> internal RunningInstance( FileSystemWatcher watcher, SafeFileHandle inotifyHandle, string directoryPath, bool includeSubdirectories, Interop.libc.NotifyEvents notifyFilters, CancellationToken cancellationToken) { Debug.Assert(watcher != null); Debug.Assert(inotifyHandle != null && !inotifyHandle.IsInvalid && !inotifyHandle.IsClosed); Debug.Assert(directoryPath != null); _weakWatcher = new WeakReference <FileSystemWatcher>(watcher); _inotifyHandle = inotifyHandle; _directoryPath = directoryPath; _buffer = watcher.AllocateBuffer(); Debug.Assert(_buffer != null && _buffer.Length > (c_INotifyEventSize + NAME_MAX + 1)); _includeSubdirectories = includeSubdirectories; _notifyFilters = notifyFilters; _cancellationToken = cancellationToken; FileSystemWatcher.CaseSensitive = true; // Add a watch for this starting directory. We keep track of the watch descriptor => directory information // mapping in a dictionary; this is needed in order to be able to determine the containing directory // for all notifications so that we can reconstruct the full path. AddDirectoryWatchUnlocked(null, directoryPath); // Schedule a task to read from the inotify queue and process the events. Task.Factory.StartNew(obj => ((RunningInstance)obj).ProcessEvents(), this, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); // PERF: As needed, we can look into making this use async I/O rather than burning // a thread that blocks in the read syscall. }
private const int NAME_MAX = 255; // from limits.h /// <summary>Initializes the instance with all state necessary to operate a watch.</summary> internal RunningInstance( FileSystemWatcher watcher, SafeFileHandle inotifyHandle, string directoryPath, bool includeSubdirectories, Interop.libc.NotifyEvents notifyFilters, CancellationToken cancellationToken) { Debug.Assert(watcher != null); Debug.Assert(inotifyHandle != null && !inotifyHandle.IsInvalid && !inotifyHandle.IsClosed); Debug.Assert(directoryPath != null); _weakWatcher = new WeakReference<FileSystemWatcher>(watcher); _inotifyHandle = inotifyHandle; _directoryPath = directoryPath; _buffer = watcher.AllocateBuffer(); Debug.Assert(_buffer != null && _buffer.Length > (c_INotifyEventSize + NAME_MAX + 1)); _includeSubdirectories = includeSubdirectories; _notifyFilters = notifyFilters; _cancellationToken = cancellationToken; FileSystemWatcher.CaseSensitive = true; // Add a watch for this starting directory. We keep track of the watch descriptor => directory information // mapping in a dictionary; this is needed in order to be able to determine the containing directory // for all notifications so that we can reconstruct the full path. AddDirectoryWatchUnlocked(null, directoryPath); // Schedule a task to read from the inotify queue and process the events. Task.Factory.StartNew(obj => ((RunningInstance)obj).ProcessEvents(), this, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default); // PERF: As needed, we can look into making this use async I/O rather than burning // a thread that blocks in the read syscall. }
/// <summary> /// Main processing loop. This is currently implemented as a synchronous operation that continually /// reads events and processes them... in the future, this could be changed to use asynchronous processing /// if the impact of using a thread-per-FileSystemWatcher is too high. /// </summary> 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; bool addWatch = false; string expandedName = null; WatchedDirectory associatedDirectoryEntry = null; if (nextEvent.wd != -1) // wd is -1 for events like IN_Q_OVERFLOW that aren't tied to a particular watch descriptor { // 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); } // 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.libc.NotifyEvents.IN_ISDIR | Interop.libc.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.libc.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; } const Interop.libc.NotifyEvents switchMask = Interop.libc.NotifyEvents.IN_Q_OVERFLOW | Interop.libc.NotifyEvents.IN_IGNORED | Interop.libc.NotifyEvents.IN_CREATE | Interop.libc.NotifyEvents.IN_DELETE | Interop.libc.NotifyEvents.IN_ACCESS | Interop.libc.NotifyEvents.IN_MODIFY | Interop.libc.NotifyEvents.IN_ATTRIB | Interop.libc.NotifyEvents.IN_MOVED_FROM | Interop.libc.NotifyEvents.IN_MOVED_TO; switch ((Interop.libc.NotifyEvents)(mask & (uint)switchMask)) { case Interop.libc.NotifyEvents.IN_Q_OVERFLOW: watcher.NotifyInternalBufferOverflowEvent(); break; case Interop.libc.NotifyEvents.IN_CREATE: watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Created, expandedName); addWatch = true; break; case Interop.libc.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.libc.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.libc.NotifyEvents.IN_ACCESS: case Interop.libc.NotifyEvents.IN_MODIFY: case Interop.libc.NotifyEvents.IN_ATTRIB: watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Changed, expandedName); break; case Interop.libc.NotifyEvents.IN_MOVED_FROM: previousEventName = expandedName; previousEventParent = isDir ? associatedDirectoryEntry : null; previousEventCookie = nextEvent.cookie; break; case Interop.libc.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; addWatch = true; // for either rename or creation, we need to update our state break; } // If the event signaled that there's a new subdirectory and if we're monitoring subdirectories, // add a watch for it. if (addWatch && isDir && _includeSubdirectories) { AddDirectoryWatch(associatedDirectoryEntry, nextEvent.name); } // 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(); } }
/// <summary> /// Maps the FileSystemWatcher's NotifyFilters enumeration to the /// corresponding Interop.libc.NotifyEvents values. /// </summary> /// <param name="filters">The filters provided the by user.</param> /// <returns>The corresponding NotifyEvents values to use with inotify.</returns> private static Interop.libc.NotifyEvents TranslateFilters(NotifyFilters filters) { Interop.libc.NotifyEvents result = 0; // We always include a few special inotify watch values that configure // the watch's behavior. result |= Interop.libc.NotifyEvents.IN_ONLYDIR | // we only allow watches on directories Interop.libc.NotifyEvents.IN_EXCL_UNLINK; // we want to stop monitoring unlinked files // For the Created and Deleted events, we need to always // register for the created/deleted inotify events, regardless // of the supplied filters values. We explicitly don't include IN_DELETE_SELF. // The Windows implementation doesn't include notifications for the root directory, // and having this for subdirectories results in duplicate notifications, one from // the parent and one from self. result |= Interop.libc.NotifyEvents.IN_CREATE | Interop.libc.NotifyEvents.IN_DELETE; // For the Changed event, which inotify events we subscribe to // are based on the NotifyFilters supplied. const NotifyFilters filtersForAccess = NotifyFilters.LastAccess; const NotifyFilters filtersForModify = NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size; const NotifyFilters filtersForAttrib = NotifyFilters.Attributes | NotifyFilters.CreationTime | NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.Security | NotifyFilters.Size; if ((filters & filtersForAccess) != 0) { result |= Interop.libc.NotifyEvents.IN_ACCESS; } if ((filters & filtersForModify) != 0) { result |= Interop.libc.NotifyEvents.IN_MODIFY; } if ((filters & filtersForAttrib) != 0) { result |= Interop.libc.NotifyEvents.IN_ATTRIB; } // For the Rename event, we'll register for the corresponding move inotify events if the // caller's NotifyFilters asks for notications related to names. const NotifyFilters filtersForMoved = NotifyFilters.FileName | NotifyFilters.DirectoryName; if ((filters & filtersForMoved) != 0) { result |= Interop.libc.NotifyEvents.IN_MOVED_FROM | Interop.libc.NotifyEvents.IN_MOVED_TO; } return(result); }