/// <summary>Removes the watched directory from our state, and optionally removes the inotify watch itself.</summary> /// <param name="directoryEntry">The directory entry to remove.</param> /// <param name="removeInotify">true to remove the inotify watch; otherwise, false. The default is true.</param> private void RemoveWatchedDirectoryUnlocked(WatchedDirectory directoryEntry, bool removeInotify) { // If the directory has children, recursively remove them (see comments on recursion in AddDirectoryWatch). if (directoryEntry.Children != null) { foreach (WatchedDirectory child in directoryEntry.Children) { RemoveWatchedDirectoryUnlocked(child, removeInotify); } directoryEntry.Children = null; } // Then remove the directory itself. _wdToPathMap.Remove(directoryEntry.WatchDescriptor); // And if the caller has requested, remove the associated inotify watch. if (removeInotify) { SysCall(fd => { // Remove the inotify watch. This could fail if our state has become inconsistent // with the state of the world (e.g. due to lost events). So we don't want failures // to throw exceptions, but we do assert to detect coding problems during debugging. long result = Interop.libc.inotify_rm_watch(fd, directoryEntry.WatchDescriptor); Debug.Assert(result >= 0); return(0); }); } }
private static async Task ExecuteSync(WatchedDirectory watchedDirectory, Stream stream) { while (true) { await Task.Delay(SyncIntervall); watchedDirectory.Sync(stream); } }
/// <summary>Removes the watched directory from our state, and optionally removes the inotify watch itself.</summary> /// <param name="directoryEntry">The directory entry to remove.</param> /// <param name="removeInotify">true to remove the inotify watch; otherwise, false. The default is true.</param> private void RemoveWatchedDirectory(WatchedDirectory directoryEntry, bool removeInotify = true) { Debug.Assert(_includeSubdirectories); lock (SyncObj) { if (directoryEntry.Parent != null) { directoryEntry.Parent.Children.Remove(directoryEntry); } RemoveWatchedDirectoryUnlocked(directoryEntry, removeInotify); } }
private static async Task ExecuteWatchedDirectory(WatchedDirectory watchedDirectory, BlockingCollection <FileEvent> queue) { while (true) { if (queue.TryTake(out var fileEvent)) { watchedDirectory.Update(fileEvent); } await Task.Delay(PollingIntervallInMilliseconds); } }
private static void Main(string[] args) { AddLogger(); var queue = new BlockingCollection <FileEvent>(); var watchedDirectory = new WatchedDirectory(); Task.Run(() => ExecuteDirectoryWatcher(queue)); Task.Run(() => ExecuteWatchedDirectory(watchedDirectory, queue)); Task.Run(() => ExecuteSync(watchedDirectory, Console.OpenStandardOutput())); Task.Delay(1000 * 60 * 60 * 24).Wait(); Console.ReadLine(); }
public AddInWatcher(AddInReloaderConfiguration config) { foreach (var addIn in config.WatchedAddIns) { foreach (var file in addIn.WatchedFiles) { var directory = Path.GetDirectoryName(file.Path); WatchedDirectory wd; if (!_watchedDirectories.TryGetValue(directory, out wd)) { wd = new WatchedDirectory(directory, InvalidateAddIn); } wd.WatchAddIn(addIn); } } }
/// <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 AddDirectoryWatch(WatchedDirectory parent, string directoryName) { lock (SyncObj) { // The read syscall on the file descriptor will block until either close is called or until // all previously added watches are removed. We don't want to rely on close, as a) that could // lead to race conditions where we inadvertently read from a recycled file descriptor, and b) // the SafeFileHandle that wraps the file descriptor can't be disposed (thus closing // the underlying file descriptor and allowing read to wake up) while there's an active ref count // against the handle, so we'd deadlock if we relied on that approach. Instead, we want to follow // the approach of removing all watches when we're done, which means we also don't want to // add any new watches once the count hits zero. if (parent == null || _wdToPathMap.Count > 0) { Debug.Assert(parent != null || _wdToPathMap.Count == 0); AddDirectoryWatchUnlocked(parent, directoryName); } } }
public WatchedDirectoryTests() { _sut = new WatchedDirectory(); }
/// <summary>Removes the watched directory from our state, and optionally removes the inotify watch itself.</summary> /// <param name="directoryEntry">The directory entry to remove.</param> /// <param name="removeInotify">true to remove the inotify watch; otherwise, false. The default is true.</param> private void RemoveWatchedDirectoryUnlocked(WatchedDirectory directoryEntry, bool removeInotify) { // If the directory has children, recursively remove them (see comments on recursion in AddDirectoryWatch). if (directoryEntry.Children != null) { foreach (WatchedDirectory child in directoryEntry.Children) { RemoveWatchedDirectoryUnlocked (child, removeInotify); } directoryEntry.Children = null; } // Then remove the directory itself. _wdToPathMap.Remove(directoryEntry.WatchDescriptor); // And if the caller has requested, remove the associated inotify watch. if (removeInotify) { SysCall(fd => { // Remove the inotify watch. This could fail if our state has become inconsistent // with the state of the world (e.g. due to lost events). So we don't want failures // to throw exceptions, but we do assert to detect coding problems during debugging. long result = Interop.libc.inotify_rm_watch(fd, directoryEntry.WatchDescriptor); Debug.Assert(result >= 0); return 0; }); } }
/// <summary>Removes the watched directory from our state, and optionally removes the inotify watch itself.</summary> /// <param name="directoryEntry">The directory entry to remove.</param> /// <param name="removeInotify">true to remove the inotify watch; otherwise, false. The default is true.</param> private void RemoveWatchedDirectory(WatchedDirectory directoryEntry, bool removeInotify = true) { Debug.Assert (_includeSubdirectories); lock (SyncObj) { if (directoryEntry.Parent != null) { directoryEntry.Parent.Children.Remove (directoryEntry); } RemoveWatchedDirectoryUnlocked (directoryEntry, removeInotify); } }
/// <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>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> /// 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 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); }