/// <summary>Adds a watch on a directory to the existing inotify handle.</summary> /// <param name="parent">The parent directory entry.</param> /// <param name="directoryName">The new directory path to monitor, relative to the root.</param> private WatchedDirectory AddDirectoryWatchUnlocked(WatchedDirectory parent, string directoryName) { string fullPath = parent != null ? parent.GetPath(false, directoryName) : directoryName; // Add a watch for the full path. If the path is already being watched, this will return // the existing descriptor. This works even in the case of a rename. int wd = (int)SysCall(fd => Interop.libc.inotify_add_watch(fd, fullPath, (uint)_notifyFilters)); // Then store the path information into our map. WatchedDirectory directoryEntry; bool isNewDirectory = false; if (_wdToPathMap.TryGetValue(wd, out directoryEntry)) { // The watch descriptor was already in the map. Hard links on directories // aren't possible, and symlinks aren't annotated as IN_ISDIR, // so this is a rename. (In extremely remote cases, this could be // a recycled watch descriptor if many, many events were lost // such that our dictionary got very inconsistent with the state // of the world, but there's little that can be done about that.) if (directoryEntry.Parent != parent) { if (directoryEntry.Parent != null) { directoryEntry.Parent.Children.Remove (directoryEntry); } directoryEntry.Parent = parent; if (parent != null) { parent.InitializedChildren.Add (directoryEntry); } } directoryEntry.Name = directoryName; } else { // The watch descriptor wasn't in the map. This is a creation. directoryEntry = new WatchedDirectory { Parent = parent, WatchDescriptor = wd, Name = directoryName }; if (parent != null) { parent.InitializedChildren.Add (directoryEntry); } _wdToPathMap.Add(wd, directoryEntry); isNewDirectory = true; } // Since inotify doesn't handle nesting implicitly, explicitly // add a watch for each child directory if the developer has // asked for subdirectories to be included. if (isNewDirectory && _includeSubdirectories) { // This method is recursive. If we expect to see hierarchies // so deep that it would cause us to overflow the stack, we could // consider using an explicit stack object rather than recursion. // This is unlikely, however, given typical directory names // and max path limits. foreach (string subDir in Directory.EnumerateDirectories(fullPath)) { AddDirectoryWatchUnlocked(directoryEntry, System.IO.Path.GetFileName(subDir)); // AddDirectoryWatchUnlocked will add the new directory to // this.Children, so we don't have to / shouldn't also do it here. } } return directoryEntry; }
/// <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>Adds a watch on a directory to the existing inotify handle.</summary> /// <param name="parent">The parent directory entry.</param> /// <param name="directoryName">The new directory path to monitor, relative to the root.</param> private void AddDirectoryWatchUnlocked(WatchedDirectory parent, string directoryName) { string fullPath = parent != null ? parent.GetPath(false, directoryName) : directoryName; // inotify_add_watch will fail if this is a symlink, so check that we didn't get a symlink Interop.Sys.FileStatus status = default(Interop.Sys.FileStatus); if ((Interop.Sys.LStat(fullPath, out status) == 0) && ((status.Mode & (uint)Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK)) { return; } // Add a watch for the full path. If the path is already being watched, this will return // the existing descriptor. This works even in the case of a rename. We also add the DONT_FOLLOW // and EXCL_UNLINK flags to keep parity with Windows where we don't pickup symlinks or unlinked // files (which don't exist in Windows) int wd = Interop.Sys.INotifyAddWatch(_inotifyHandle, fullPath, (uint)(this._notifyFilters | Interop.Sys.NotifyEvents.IN_DONT_FOLLOW | Interop.Sys.NotifyEvents.IN_EXCL_UNLINK)); if (wd == -1) { // If we get an error when trying to add the watch, don't let that tear down processing. Instead, // raise the Error event with the exception and let the user decide how to handle it. Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo(); Exception exc; if (error.Error == Interop.Error.ENOSPC) { string maxValue = ReadMaxUserLimit(MaxUserWatchesPath); string message = !string.IsNullOrEmpty(maxValue) ? SR.Format(SR.IOException_INotifyWatchesUserLimitExceeded_Value, maxValue) : SR.IOException_INotifyWatchesUserLimitExceeded; exc = new IOException(message, error.RawErrno); } else { exc = Interop.GetExceptionForIoErrno(error, fullPath); } FileSystemWatcher watcher; if (_weakWatcher.TryGetTarget(out watcher)) { watcher.OnError(new ErrorEventArgs(exc)); } return; } // Then store the path information into our map. WatchedDirectory directoryEntry; bool isNewDirectory = false; if (_wdToPathMap.TryGetValue(wd, out directoryEntry)) { // The watch descriptor was already in the map. Hard links on directories // aren't possible, and symlinks aren't annotated as IN_ISDIR, // so this is a rename. (In extremely remote cases, this could be // a recycled watch descriptor if many, many events were lost // such that our dictionary got very inconsistent with the state // of the world, but there's little that can be done about that.) if (directoryEntry.Parent != parent) { if (directoryEntry.Parent != null) { directoryEntry.Parent.Children.Remove (directoryEntry); } directoryEntry.Parent = parent; if (parent != null) { parent.InitializedChildren.Add (directoryEntry); } } directoryEntry.Name = directoryName; } else { // The watch descriptor wasn't in the map. This is a creation. directoryEntry = new WatchedDirectory { Parent = parent, WatchDescriptor = wd, Name = directoryName }; if (parent != null) { parent.InitializedChildren.Add (directoryEntry); } _wdToPathMap.Add(wd, directoryEntry); isNewDirectory = true; } // Since inotify doesn't handle nesting implicitly, explicitly // add a watch for each child directory if the developer has // asked for subdirectories to be included. if (isNewDirectory && _includeSubdirectories) { // This method is recursive. If we expect to see hierarchies // so deep that it would cause us to overflow the stack, we could // consider using an explicit stack object rather than recursion. // This is unlikely, however, given typical directory names // and max path limits. foreach (string subDir in Directory.EnumerateDirectories(fullPath)) { AddDirectoryWatchUnlocked(directoryEntry, System.IO.Path.GetFileName(subDir)); // AddDirectoryWatchUnlocked will add the new directory to // this.Children, so we don't have to / shouldn't also do it here. } } }
/// <summary>Adds a watch on a directory to the existing inotify handle.</summary> /// <param name="parent">The parent directory entry.</param> /// <param name="directoryName">The new directory path to monitor, relative to the root.</param> private WatchedDirectory AddDirectoryWatchUnlocked(WatchedDirectory parent, string directoryName) { string fullPath = parent != null?parent.GetPath(false, directoryName) : directoryName; // Add a watch for the full path. If the path is already being watched, this will return // the existing descriptor. This works even in the case of a rename. int wd = (int)SysCall(fd => Interop.libc.inotify_add_watch(fd, fullPath, (uint)_notifyFilters)); // Then store the path information into our map. WatchedDirectory directoryEntry; bool isNewDirectory = false; if (_wdToPathMap.TryGetValue(wd, out directoryEntry)) { // The watch descriptor was already in the map. Hard links on directories // aren't possible, and symlinks aren't annotated as IN_ISDIR, // so this is a rename. (In extremely remote cases, this could be // a recycled watch descriptor if many, many events were lost // such that our dictionary got very inconsistent with the state // of the world, but there's little that can be done about that.) if (directoryEntry.Parent != parent) { if (directoryEntry.Parent != null) { directoryEntry.Parent.Children.Remove(directoryEntry); } directoryEntry.Parent = parent; if (parent != null) { parent.InitializedChildren.Add(directoryEntry); } } directoryEntry.Name = directoryName; } else { // The watch descriptor wasn't in the map. This is a creation. directoryEntry = new WatchedDirectory { Parent = parent, WatchDescriptor = wd, Name = directoryName }; if (parent != null) { parent.InitializedChildren.Add(directoryEntry); } _wdToPathMap.Add(wd, directoryEntry); isNewDirectory = true; } // Since inotify doesn't handle nesting implicitly, explicitly // add a watch for each child directory if the developer has // asked for subdirectories to be included. if (isNewDirectory && _includeSubdirectories) { // This method is recursive. If we expect to see hierarchies // so deep that it would cause us to overflow the stack, we could // consider using an explicit stack object rather than recursion. // This is unlikely, however, given typical directory names // and max path limits. foreach (string subDir in Directory.EnumerateDirectories(fullPath)) { AddDirectoryWatchUnlocked(directoryEntry, System.IO.Path.GetFileName(subDir)); // AddDirectoryWatchUnlocked will add the new directory to // this.Children, so we don't have to / shouldn't also do it here. } } return(directoryEntry); }