diff --git a/src/libraries/Common/src/Interop/Linux/System.Native/Interop.INotify.cs b/src/libraries/Common/src/Interop/Linux/System.Native/Interop.INotify.cs
index 446bdb4ada25f6..4960583633e2b6 100644
--- a/src/libraries/Common/src/Interop/Linux/System.Native/Interop.INotify.cs
+++ b/src/libraries/Common/src/Interop/Linux/System.Native/Interop.INotify.cs
@@ -56,6 +56,7 @@ internal enum NotifyEvents
IN_ONLYDIR = 0x01000000,
IN_DONT_FOLLOW = 0x02000000,
IN_EXCL_UNLINK = 0x04000000,
+ IN_MASK_ADD = 0x20000000,
IN_ISDIR = 0x40000000,
}
}
diff --git a/src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj b/src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj
index 438efa515b3964..71bcacb55d9236 100644
--- a/src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj
+++ b/src/libraries/System.IO.FileSystem.Watcher/src/System.IO.FileSystem.Watcher.csproj
@@ -133,6 +133,8 @@
+
+
diff --git a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs
index 4687691395fbb4..2104239dcdd1bb 100644
--- a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs
+++ b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs
@@ -1,21 +1,51 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
+using System.Threading.Channels;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
namespace System.IO
{
- // Note: This class has an OS Limitation where the inotify API can miss events if a directory is created and immediately has
- // changes underneath. This is due to the inotify* APIs not being recursive and needing to call inotify_add_watch on
- // each subdirectory, causing a race between adding the watch and file system events happening.
+ // Implementation notes:
+ //
+ // Missed events for recursive watching:
+ // The inotify APIs are not recursive. We need to call inotify_add_watch when we detect a child directory to track it.
+ // Events that occurred on the directory before we've added it will be lost.
+ //
+ // Path vs directory:
+ // Note that inotify does not watch a path, but it watches directories.
+ // When a path is passed to inotify_add_watch, the directory is looked up by the kernel and a watch descriptor (wd) is returned for watching that directory.
+ // If the directory is moved to a different path, inotify will continue to reports its events.
+ // If we have previously added a watch for a path, and we call inotify_add_watch again for that path then:
+ // - if the looked up directory is still the same, the same wd will be returned, or
+ // - if the path now refers to a different directory, another wd will be returned.
+ //
+ // For each FileSystemWatcher we use a Watcher object that represents all inotify operations performed for that FileSystemWatcher.
+ // To represent the difference explained above (path vs directory) we use a WatchedDirectory object to represent a path that is watched
+ // and a separate Watch object that represent the wd returned by the inotify_add_watch.
+ // Each WatchedDirectory has a single Watch, while a Watch may be used by several WatchDirectories.
+ // When there are no more WatchDirectories using the Watch, we can remove it.
+ //
+ // Locking:
+ // To prevent deadlocks, the locks (as needed) should be taken in this order: s_watchersLock, _addLock, lock on Watcher instance, lock on Watch instance.
+ //
+ // Shared inotify instance:
+ // By default, the number of inotify instances per user is limited to 128.
+ // Because of this low limit, we make all the FileSystemWatchers share a single inotify instance to reduce contention with other processes.
+ // A dedicated thread dequeues the inotify events. From the inotify events, FileSystemWatcher events are emitted from the ThreadPool.
+ // This stops FileSystemWatcher event handlers to block one another, or them blocking the inotify thread which could cause the inotify event queue to overflow.
+ // This requires us to use IN_MASK_ADD which may cause us to continue receive events that no FileSystemWatcher is still interested in.
public partial class FileSystemWatcher
{
+ private const int PATH_MAX = 4096;
+
/// Starts a new watch operation if one is not currently running.
private void StartRaisingEvents()
{
@@ -26,76 +56,16 @@ private void StartRaisingEvents()
return;
}
- // If we already have a cancellation object, we're already running.
- if (_cancellation != null)
+ // If we already have a watcher object, we're already running.
+ if (_watcher != null)
{
return;
}
- // Open an inotify file descriptor.
- SafeFileHandle handle = Interop.Sys.INotifyInit();
- if (handle.IsInvalid)
- {
- Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
- handle.Dispose();
- switch (error.Error)
- {
- case Interop.Error.EMFILE:
- string? maxValue = ReadMaxUserLimit(MaxUserInstancesPath);
- string message = !string.IsNullOrEmpty(maxValue) ?
- SR.Format(SR.IOException_INotifyInstanceUserLimitExceeded_Value, maxValue) :
- SR.IOException_INotifyInstanceUserLimitExceeded;
- throw new IOException(message, error.RawErrno);
- case Interop.Error.ENFILE:
- throw new IOException(SR.IOException_INotifyInstanceSystemLimitExceeded, error.RawErrno);
- default:
- throw Interop.GetExceptionForIoErrno(error);
- }
- }
-
- try
- {
- // Create the cancellation object that will be used by this FileSystemWatcher to cancel the new watch operation
- CancellationTokenSource cancellation = new CancellationTokenSource();
-
- // Start running. All state associated with the watch operation is stored in a separate object; this is done
- // to avoid race conditions that could result if the users quickly starts/stops/starts/stops/etc. causing multiple
- // active operations to all be outstanding at the same time.
- var runner = new RunningInstance(
- this, handle, _directory,
- IncludeSubdirectories, NotifyFilter, cancellation.Token);
-
- // Now that we've created the runner, store the cancellation object and mark the instance
- // as running. We wait to do this so that if there was a failure, StartRaisingEvents
- // may be called to try again without first having to call StopRaisingEvents.
- _cancellation = cancellation;
- _enabled = true;
+ _watcher = new INotify.Watcher(this);
+ _enabled = true;
- // Start the runner
- runner.Start();
- }
- catch
- {
- // If we fail to actually start the watching even though we've opened the
- // inotify handle, close the inotify handle proactively rather than waiting for it
- // to be finalized.
- handle.Dispose();
- throw;
- }
- }
-
- /// Allocates a buffer of the requested internal buffer size.
- /// The allocated buffer.
- private byte[] AllocateBuffer()
- {
- try
- {
- return new byte[_internalBufferSize];
- }
- catch (OutOfMemoryException)
- {
- throw new OutOfMemoryException(SR.Format(SR.BufferSizeTooLarge, _internalBufferSize));
- }
+ _watcher.Start();
}
/// Cancels the currently running watch operation if there is one.
@@ -106,21 +76,14 @@ private void StopRaisingEvents()
if (IsSuspended())
return;
- // If there's an active cancellation token, cancel and release it.
- // The cancellation token and the processing task respond to cancellation
- // to handle all other cleanup.
- var cts = _cancellation;
- if (cts != null)
- {
- _cancellation = null;
- cts.Cancel();
- }
+ _watcher?.Stop();
+ _watcher = null;
}
/// Called when FileSystemWatcher is finalized.
private void FinalizeDispose()
{
- // The RunningInstance remains rooted and holds open the SafeFileHandle until it's explicitly
+ // The Watcher remains rooted and holds open the SafeFileHandle until it's explicitly
// torn down. FileSystemWatcher.Dispose will call StopRaisingEvents, but not on finalization;
// thus we need to explicitly call it here.
StopRaisingEvents();
@@ -132,11 +95,7 @@ private void FinalizeDispose()
/// Path to the procfs file that contains the maximum number of inotify watches an individual user may create.
private const string MaxUserWatchesPath = "/proc/sys/fs/inotify/max_user_watches";
- ///
- /// Cancellation for the currently running watch operation.
- /// This is non-null if an operation has been started and null if stopped.
- ///
- private CancellationTokenSource? _cancellation;
+ private INotify.Watcher? _watcher;
/// Reads the value of a max user limit path from procfs.
/// The path to read.
@@ -147,656 +106,728 @@ private void FinalizeDispose()
catch { return null; }
}
- ///
- /// Maps the FileSystemWatcher's NotifyFilters enumeration to the
- /// corresponding Interop.Sys.NotifyEvents values.
- ///
- /// The filters provided the by user.
- /// The corresponding NotifyEvents values to use with inotify.
- private static Interop.Sys.NotifyEvents TranslateFilters(NotifyFilters filters)
- {
- Interop.Sys.NotifyEvents result = 0;
-
- // We always include a few special inotify watch values that configure
- // the watch's behavior.
- result |=
- Interop.Sys.NotifyEvents.IN_ONLYDIR | // we only allow watches on directories
- Interop.Sys.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.Sys.NotifyEvents.IN_CREATE |
- Interop.Sys.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.Sys.NotifyEvents.IN_ACCESS;
- }
- if ((filters & filtersForModify) != 0)
- {
- result |= Interop.Sys.NotifyEvents.IN_MODIFY;
- }
- if ((filters & filtersForAttrib) != 0)
- {
- result |= Interop.Sys.NotifyEvents.IN_ATTRIB;
- }
-
- // For the Rename event, we'll register for the corresponding move inotify events if the
- // caller's NotifyFilters asks for notifications related to names.
- const NotifyFilters filtersForMoved =
- NotifyFilters.FileName |
- NotifyFilters.DirectoryName;
- if ((filters & filtersForMoved) != 0)
- {
- result |=
- Interop.Sys.NotifyEvents.IN_MOVED_FROM |
- Interop.Sys.NotifyEvents.IN_MOVED_TO;
- }
-
- return result;
- }
-
- ///
- /// State and processing associated with an active watch operation. This state is kept separate from FileSystemWatcher to avoid
- /// race conditions when a user starts/stops/starts/stops/etc. in quick succession, resulting in the potential for multiple
- /// active operations. It also helps with avoiding rooted cycles and enabling proper finalization.
- ///
- private sealed class RunningInstance
+ private sealed class INotify
{
///
/// The size of the native struct inotify_event. 4 32-bit integer values, the last of which is a length
/// that indicates how many bytes follow to form the string name.
///
- private const int c_INotifyEventSize = 16;
-
- ///
- /// Weak reference to the associated watcher. A weak reference is used so that the FileSystemWatcher may be collected and finalized,
- /// causing an active operation to be torn down. With a strong reference, a blocking read on the inotify handle will keep alive this
- /// instance which will keep alive the FileSystemWatcher which will not be finalizable and thus which will never signal to the blocking
- /// read to wake up in the event that the user neglects to stop raising events.
- ///
- private readonly WeakReference _weakWatcher;
- ///
- /// The path for the primary watched directory.
- ///
- private readonly string _directoryPath;
- ///
- /// The inotify handle / file descriptor
- ///
+ private const int INotifyEventSize = 16;
+
+ // The name buffer in struct inotify_event is 0-256 bytes making the total inotify_event size 16-272 bytes.
+ // The below buffer fits at 60+ events of the largest events.
+ // For a typical file name size of <32 bytes, we can receive 300+ events in a single read.
+ // This buffer size is assumed to be be plenty because the read loop dispatches the work for user event handling to the ThreadPool and then performs a new read.
+ private const int BufferSize = 16384;
+
+ // Guards the watchers of the inotify instance and starting the inotify thread.
+ private static readonly object s_watchersLock = new();
+ private static INotify? s_currentInotify;
+ private readonly List _watchers = new();
+ private readonly byte[] _buffer = new byte[BufferSize];
private readonly SafeFileHandle _inotifyHandle;
- ///
- /// Buffer used to store raw bytes read from the inotify handle.
- ///
- private readonly byte[] _buffer;
- ///
- /// The number of bytes read into the _buffer.
- ///
+ private readonly ConcurrentDictionary _wdToWatch = new ConcurrentDictionary();
+ private readonly ReaderWriterLockSlim _addLock = new(LockRecursionPolicy.NoRecursion);
+ private bool _isThreadStopping;
+ private bool _allWatchersStopped;
private int _bufferAvailable;
- ///
- /// The next position in _buffer from which an event should be read.
- ///
private int _bufferPos;
- ///
- /// Filters to use when adding a watch on directories.
- ///
- private readonly NotifyFilters _notifyFilters;
- private readonly Interop.Sys.NotifyEvents _watchFilters;
- ///
- /// Whether to monitor subdirectories. Unlike Win32, inotify does not implicitly monitor subdirectories;
- /// watches must be explicitly added for those subdirectories.
- ///
- private readonly bool _includeSubdirectories;
- ///
- /// Token to monitor for cancellation requests, upon which processing is stopped and all
- /// state is cleaned up.
- ///
- private readonly CancellationToken _cancellationToken;
- ///
- /// Mapping from watch descriptor (as returned by inotify_add_watch) to state for
- /// the associated directory being watched. Events from inotify include only relative
- /// names, so the watch descriptor in an event must be used to look up the associated
- /// directory path in order to convert the relative filename into a full path.
- ///
- private readonly Dictionary _wdToPathMap = new Dictionary();
- ///
- /// Maximum length of a name returned from inotify event.
- ///
- private const int NAME_MAX = 255; // from limits.h
+ private WatchedDirectory[] _dirBuffer = new WatchedDirectory[4];
- /// Initializes the instance with all state necessary to operate a watch.
- internal RunningInstance(
- FileSystemWatcher watcher, SafeFileHandle inotifyHandle, string directoryPath,
- bool includeSubdirectories, NotifyFilters notifyFilters, CancellationToken cancellationToken)
+ public INotify()
{
- Debug.Assert(watcher != null);
- Debug.Assert(inotifyHandle != null && !inotifyHandle.IsInvalid && !inotifyHandle.IsClosed);
- Debug.Assert(directoryPath != null);
-
- _weakWatcher = new WeakReference(watcher);
- _inotifyHandle = inotifyHandle;
- _directoryPath = directoryPath;
- _buffer = watcher.AllocateBuffer();
- Debug.Assert(_buffer != null && _buffer.Length > (c_INotifyEventSize + NAME_MAX + 1));
- _includeSubdirectories = includeSubdirectories;
- _notifyFilters = notifyFilters;
- _watchFilters = TranslateFilters(notifyFilters);
- _cancellationToken = cancellationToken;
-
- // 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);
+ _inotifyHandle = CreateINotifyHandle();
+
+ static SafeFileHandle CreateINotifyHandle()
+ {
+ SafeFileHandle handle = Interop.Sys.INotifyInit();
+
+ if (handle.IsInvalid)
+ {
+ Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
+ handle.Dispose();
+ switch (error.Error)
+ {
+ case Interop.Error.EMFILE:
+ string? maxValue = ReadMaxUserLimit(MaxUserInstancesPath);
+ string message = !string.IsNullOrEmpty(maxValue) ?
+ SR.Format(SR.IOException_INotifyInstanceUserLimitExceeded_Value, maxValue) :
+ SR.IOException_INotifyInstanceUserLimitExceeded;
+ throw new IOException(message, error.RawErrno);
+ case Interop.Error.ENFILE:
+ throw new IOException(SR.IOException_INotifyInstanceSystemLimitExceeded, error.RawErrno);
+ default:
+ throw Interop.GetExceptionForIoErrno(error);
+ }
+ }
+
+ return handle;
+ }
+ }
+
+ private bool IsStopped => _isThreadStopping || _allWatchersStopped;
+
+ private void AddWatcher(Watcher watcher)
+ {
+ Debug.Assert(Monitor.IsEntered(s_watchersLock));
+ _watchers.Add(watcher);
}
- internal void Start()
+ private void StartThread()
{
- // Spawn a thread to read from the inotify queue and process the events.
- new Thread(obj => ((RunningInstance)obj!).ProcessEvents())
+ Debug.Assert(Monitor.IsEntered(s_watchersLock));
+
+ try
+ {
+ // Spawn a thread to read from the inotify queue and process the events.
+ Thread thread = new Thread(obj => ((INotify)obj!).ProcessEvents())
+ {
+ IsBackground = true,
+ Name = ".NET File Watcher"
+ };
+ thread.Start(this);
+ }
+ catch
{
- IsBackground = true,
- Name = ".NET File Watcher"
- }.Start(this);
+ StopINotify();
- // PERF: As needed, we can look into making this use async I/O rather than burning
- // a thread that blocks in the read syscall.
+ throw;
+ }
}
- /// Object to use for synchronizing access to state when necessary.
- private object SyncObj { get { return _wdToPathMap; } }
+ private void StopINotify()
+ {
+ // This method gets called only on the ProcessEvents thread, or when that thread fails to start.
+ // It closes the inotify handle.
+ Debug.Assert(!_isThreadStopping);
+ _isThreadStopping = true;
- /// Adds a watch on a directory to the existing inotify handle.
- /// The parent directory entry.
- /// The new directory path to monitor, relative to the root.
- private void AddDirectoryWatch(WatchedDirectory parent, string directoryName)
+ // Sync with AddOrUpdateWatchedDirectory and RemoveUnusedINotifyWatches.
+ _addLock.EnterWriteLock();
+ _addLock.ExitWriteLock();
+
+ // Close the handle.
+ _inotifyHandle.Dispose();
+ }
+
+ private WatchedDirectory? AddOrUpdateWatchedDirectory(Watcher watcher, WatchedDirectory? parent, string directoryPath, Interop.Sys.NotifyEvents watchFilters, bool followLinks = false, bool ignoreMissing = true)
{
- lock (SyncObj)
+ Debug.Assert(!Monitor.IsEntered(watcher)); // We musn't hold the watcher lock prior to taking the _addLock.
+
+ WatchedDirectory? inotifyWatchesToRemove = null;
+ WatchedDirectory dir;
+
+ // This locks prevents removing watches while watches are being added.
+ // It is also used to synchronize with Stop.
+ _addLock.EnterReadLock();
+ try
{
- // 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 (_wdToPathMap.Count > 0)
+ // Serialize adding watches to the same watcher.
+ // Concurrently adding watches may happen during the initial reursive iteration of the directory.
+ // This ensures the WatchedDirectory matches with the most recent INotifyAddWatch directory.
+ lock (watcher)
{
- AddDirectoryWatchUnlocked(parent, directoryName);
+ if (_isThreadStopping // inotify thread stopping
+ || watcher.IsStopped // user stopped raising events
+ || (parent is not null && watcher.RootDirectory is null)) // process events removed the root
+ {
+ return null;
+ }
+
+ Interop.Sys.NotifyEvents mask = watchFilters |
+ Interop.Sys.NotifyEvents.IN_ONLYDIR | // we only allow watches on directories
+ Interop.Sys.NotifyEvents.IN_EXCL_UNLINK | // we want to stop monitoring unlinked files
+ (followLinks ? 0 : Interop.Sys.NotifyEvents.IN_DONT_FOLLOW |
+ Interop.Sys.NotifyEvents.IN_MASK_ADD);
+
+ // To support multiple FileSystemWatchers on the same inotify instance, we need to use IN_MASK_ADD
+ // so we don't remove events another watcher is interested in.
+ // The downside is that we won't unsubscribe from events that are unique to a watcher when it stops.
+ mask |= Interop.Sys.NotifyEvents.IN_MASK_ADD;
+
+ if (watcher.IncludeSubdirectories)
+ {
+ mask |= Interop.Sys.NotifyEvents.IN_CREATE | Interop.Sys.NotifyEvents.IN_MOVED_TO | Interop.Sys.NotifyEvents.IN_MOVED_FROM;
+ }
+
+ int wd = Interop.Sys.INotifyAddWatch(_inotifyHandle, directoryPath, (uint)mask);
+ 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();
+
+ // Don't report an error when we can't add a watch because the child directory was removed or replaced by a file.
+ if (ignoreMissing && (error.Error == Interop.Error.ENOENT || error.Error == Interop.Error.ENOTDIR))
+ {
+ return null;
+ }
+
+ 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, directoryPath);
+ }
+
+ watcher.QueueError(exc);
+
+ return null;
+ }
+
+ Watch watch = _wdToWatch.AddOrUpdate(wd, (int wd) => new Watch(wd), (int wd, Watch current) => current);
+
+ if (parent is null)
+ {
+ Debug.Assert(watcher.RootDirectory is null);
+ dir = new WatchedDirectory(watch, watcher, "", parent);
+ watcher.RootDirectory = dir;
+ }
+ else
+ {
+ // Check if the parent already has a watch for this child name.
+ string name = System.IO.Path.GetFileName(directoryPath);
+ int idx = parent.FindChild(name);
+ if (idx != -1)
+ {
+ dir = parent.Children![idx];
+ if (dir.Watch == watch)
+ {
+ // The inotify watch is the same.
+ return dir;
+ }
+
+ // The current watch is watching a different directory, use the new watch instead.
+ bool removeINotifyWatches = false;
+
+ RemoveWatchedDirectoryFromParentAndWatches(dir, ref removeINotifyWatches);
+
+ if (removeINotifyWatches)
+ {
+ inotifyWatchesToRemove = dir;
+ }
+ }
+ dir = new WatchedDirectory(watch, watcher, name, parent);
+ parent.InitializedChildren.Add(dir);
+ }
+
+ lock (watch)
+ {
+ watch.Watchers.Add(dir);
+ }
}
}
+ finally
+ {
+ _addLock.ExitReadLock();
+ }
+
+ if (inotifyWatchesToRemove is not null)
+ {
+ RemoveUnusedINotifyWatches(inotifyWatchesToRemove);
+ }
+
+ return dir;
}
- /// Adds a watch on a directory to the existing inotify handle.
- /// The parent directory entry.
- /// The new directory path to monitor, relative to the root.
- private void AddDirectoryWatchUnlocked(WatchedDirectory? parent, string directoryName)
+ private void RemoveWatchedDirectory(WatchedDirectory dir, int ignoredFd = -1)
{
- bool hasParent = parent != null;
- string fullPath = hasParent ? parent!.GetPath(false, directoryName) : directoryName;
+ bool removeINotifyWatches = false;
- // inotify_add_watch will fail if this is a symlink, so check that we didn't get a symlink
- // with the exception of the watched directory where we try to dereference the path.
- if (hasParent &&
- (Interop.Sys.LStat(fullPath, out Interop.Sys.FileStatus status) == 0) &&
- ((status.Mode & (uint)Interop.Sys.FileTypes.S_IFMT) == Interop.Sys.FileTypes.S_IFLNK))
- {
- return;
- }
+ RemoveWatchedDirectoryFromParentAndWatches(dir, ref removeINotifyWatches);
- // 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 (for subdirectories only)
- // and EXCL_UNLINK flags to keep parity with Windows where we don't pickup symlinks or unlinked
- // files (which don't exist in Windows)
- uint mask = (uint)(_watchFilters | Interop.Sys.NotifyEvents.IN_EXCL_UNLINK | (hasParent ? Interop.Sys.NotifyEvents.IN_DONT_FOLLOW : 0));
- int wd = Interop.Sys.INotifyAddWatch(_inotifyHandle, fullPath, mask);
- if (wd == -1)
+ if (removeINotifyWatches)
{
- // 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.
+ RemoveUnusedINotifyWatches(dir, ignoredFd);
+ }
+ }
- Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
+ private void RemoveUnusedINotifyWatches(WatchedDirectory removedDir, int ignoredFd = -1)
+ {
+ Debug.Assert(!Monitor.IsEntered(removedDir.Watcher)); // We musn't hold the watcher lock prior to taking the _addLock.
- // Don't report an error when we can't add a watch because the child directory
- // was removed or replaced by a file.
- if (hasParent && (error.Error == Interop.Error.ENOENT ||
- error.Error == Interop.Error.ENOTDIR))
+ // _addLock stops handles from being added while we'll removing watches.
+ // This is needed to prevent removing watch descriptors between INotifyAddWatch and adding them to the Watch.Watchers.
+ // _addLock is also used to synchronizes with Stop.
+ _addLock.EnterWriteLock();
+ try
+ {
+ if (_isThreadStopping)
{
return;
}
- 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
+ RemoveINotifyWatchWhenNoMoreWatchers(removedDir.Watch, ignoredFd);
+
+ // We don't need to remove the children when all watchers have stopped and the inotify will be closed.
+ if (_allWatchersStopped)
{
- exc = Interop.GetExceptionForIoErrno(error, fullPath);
+ return;
}
- if (_weakWatcher.TryGetTarget(out FileSystemWatcher? watcher))
+ lock (removedDir.Watcher)
{
- watcher.OnError(new ErrorEventArgs(exc));
+ if (removedDir.Children is { } children)
+ {
+ foreach (var child in children)
+ {
+ RemoveINotifyWatchWhenNoMoreWatchers(child.Watch, ignoredFd);
+ }
+ }
}
-
- return;
+ }
+ finally
+ {
+ _addLock.ExitWriteLock();
}
- // Then store the path information into our map.
- WatchedDirectory? directoryEntry;
- bool isNewDirectory = false;
- if (_wdToPathMap.TryGetValue(wd, out directoryEntry))
+ void RemoveINotifyWatchWhenNoMoreWatchers(Watch watch, int ignoredFd)
{
- // 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)
+ lock (watch)
{
- // Work around https://github.com/dotnet/csharplang/issues/3393 preventing Parent?.Children!. from behaving as expected
- if (directoryEntry.Parent != null)
+ if (watch.Watchers.Count == 0)
{
- directoryEntry.Parent.Children!.Remove(directoryEntry);
- }
-
- directoryEntry.Parent = parent;
- if (hasParent)
- {
- parent!.InitializedChildren.Add(directoryEntry);
+ if (_wdToWatch.TryRemove(watch.WatchDescriptor, out _))
+ {
+ if (watch.WatchDescriptor != ignoredFd)
+ {
+ Interop.Sys.INotifyRemoveWatch(_inotifyHandle, watch.WatchDescriptor);
+ }
+ }
}
}
- directoryEntry.Name = directoryName;
}
- else
+ }
+
+ private void RemoveWatchedDirectoryFromParentAndWatches(WatchedDirectory dir, ref bool removeINotifyWatches)
+ {
+ if (dir.IsRootDir)
{
- // The watch descriptor wasn't in the map. This is a creation.
- directoryEntry = new WatchedDirectory
+ lock (s_watchersLock)
{
- Parent = parent,
- WatchDescriptor = wd,
- Name = directoryName
- };
- if (hasParent)
- {
- parent!.InitializedChildren.Add(directoryEntry);
+ _watchers.Remove(dir.Watcher);
+
+ // Set _allWatchersStopped before we update the Watch and _wdToWatch.
+ _allWatchersStopped = _watchers.Count == 0;
}
- _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)
+ Watcher watcher = dir.Watcher;
+ lock (watcher)
{
- try
+ if (dir.IsRootDir)
{
- // 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))
+ if (watcher.RootDirectory == null)
{
- 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; // Already removed.
}
+ watcher.RootDirectory = null;
}
- catch (DirectoryNotFoundException)
- { } // The child directory was removed.
- catch (IOException ex) when (ex.HResult == Interop.Error.ENOTDIR.Info().RawErrno)
- { } // The child directory was replaced by a file.
- catch (Exception ex)
+ else
+ {
+ Debug.Assert(dir.Parent is not null); // !IsRootDirectory
+ int idx = dir.Parent.FindChild(dir.Name);
+ Debug.Assert(idx != -1);
+ if (idx == -1)
+ {
+ return; // Already removed.
+ }
+ dir.Parent.Children!.RemoveAt(idx);
+ }
+
+ RemoveFromWatch(dir, ref removeINotifyWatches);
+
+ if (dir.Children is { } children)
{
- if (_weakWatcher.TryGetTarget(out FileSystemWatcher? watcher))
+ foreach (var child in children)
{
- watcher.OnError(new ErrorEventArgs(ex));
+ RemoveFromWatch(child, ref removeINotifyWatches);
}
}
}
- }
- /// Removes the watched directory from our state, and optionally removes the inotify watch itself.
- /// The directory entry to remove.
- /// true to remove the inotify watch; otherwise, false. The default is true.
- private void RemoveWatchedDirectory(WatchedDirectory directoryEntry, bool removeInotify = true)
- {
- Debug.Assert(_includeSubdirectories);
- lock (SyncObj)
+ static void RemoveFromWatch(WatchedDirectory dir, ref bool removeINotifyWatches)
{
- // Work around https://github.com/dotnet/csharplang/issues/3393 preventing Parent?.Children!. from behaving as expected
- if (directoryEntry.Parent != null)
+ Watch watch = dir.Watch;
+ lock (watch)
{
- directoryEntry.Parent.Children!.Remove(directoryEntry);
+ watch.Watchers.Remove(dir);
+ removeINotifyWatches |= watch.Watchers.Count == 0;
}
-
- RemoveWatchedDirectoryUnlocked(directoryEntry, removeInotify);
}
}
- /// Removes the watched directory from our state, and optionally removes the inotify watch itself.
- /// The directory entry to remove.
- /// true to remove the inotify watch; otherwise, false. The default is true.
- private void RemoveWatchedDirectoryUnlocked(WatchedDirectory directoryEntry, bool removeInotify)
+ private void ProcessEvents()
{
- // If the directory has children, recursively remove them (see comments on recursion in AddDirectoryWatch).
- if (directoryEntry.Children != null)
+ try
{
- foreach (WatchedDirectory child in directoryEntry.Children)
+ lock (s_watchersLock)
{
- RemoveWatchedDirectoryUnlocked(child, removeInotify);
+ // We've started an INotify, but no root watch was created for the FileSystemWatcher that started it.
+ if (_watchers.Count == 0)
+ {
+ StopINotify();
+ return;
+ }
}
- directoryEntry.Children = null;
- }
- // Then remove the directory itself.
- _wdToPathMap.Remove(directoryEntry.WatchDescriptor);
+ // Carry over information from MOVED_FROM to MOVED_TO events.
+ int movedFromWatchCount = 0;
+ string movedFromName = "";
+ uint movedFromCookie = 0;
+ bool movedFromIsDir = false;
- // And if the caller has requested, remove the associated inotify watch.
- if (removeInotify)
- {
- // 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.
- int result = Interop.Sys.INotifyRemoveWatch(_inotifyHandle, directoryEntry.WatchDescriptor);
- Debug.Assert(result >= 0);
+ NotifyEvent nextEvent;
+ while (TryReadEvent(out nextEvent))
+ {
+ if (!ProcessEvent(nextEvent, ref movedFromWatchCount, ref movedFromName, ref movedFromCookie, ref movedFromIsDir))
+ break;
+ }
}
- }
-
- ///
- /// Callback invoked when cancellation is requested. Removes all watches,
- /// which will cause the active processing loop to shutdown.
- ///
- private void CancellationCallback()
- {
- lock (SyncObj)
+ catch (Exception ex)
{
- // Remove all watches (inotiy_rm_watch) and clear out the map.
- // No additional watches will be added after this point.
- foreach (int wd in this._wdToPathMap.Keys)
+ lock (s_watchersLock)
{
- int result = Interop.Sys.INotifyRemoveWatch(_inotifyHandle, wd);
- Debug.Assert(result >= 0); // ignore errors; they're non-fatal, but they also shouldn't happen
- }
+ StopINotify();
- _wdToPathMap.Clear();
+ foreach (var watcher in _watchers)
+ {
+ watcher.QueueError(ex);
+ }
+ }
+ }
+ finally
+ {
+ Debug.Assert(_inotifyHandle.IsClosed);
}
}
- ///
- /// Processes the next event. Method does not inline to prevent a strong reference to the watcher.
- ///
- /// The next event.
- /// The previous event's name.
- /// The previous event's parent.
- /// The previous event's cookie.
- /// if we can continue processing events, otherwise.
- [MethodImpl(MethodImplOptions.NoInlining)]
- private bool ProcessEvent(NotifyEvent nextEvent, ref ReadOnlySpan previousEventName, ref WatchedDirectory? previousEventParent, ref uint previousEventCookie)
+ private bool ProcessEvent(NotifyEvent nextEvent, ref int movedFromWatchCount, ref string movedFromName, ref uint movedFromCookie, ref bool movedFromIsDir)
{
- // 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))
+ // Subset of EventMask that are emitted conditionally based on NotifyFilters.DirectoryName/FileName.
+ const Interop.Sys.NotifyEvents FileDirEvents =
+ Interop.Sys.NotifyEvents.IN_CREATE |
+ Interop.Sys.NotifyEvents.IN_DELETE |
+ Interop.Sys.NotifyEvents.IN_MOVED_FROM |
+ Interop.Sys.NotifyEvents.IN_MOVED_TO;
+ // NotifyEvents that generate FileSystemWatcher events.
+ const Interop.Sys.NotifyEvents EventMask =
+ FileDirEvents |
+ Interop.Sys.NotifyEvents.IN_ACCESS |
+ Interop.Sys.NotifyEvents.IN_MODIFY |
+ Interop.Sys.NotifyEvents.IN_ATTRIB;
+
+ Span pathBuffer = stackalloc char[PATH_MAX];
+ Interop.Sys.NotifyEvents mask = (Interop.Sys.NotifyEvents)nextEvent.mask;
+
+ // An overflow event means we missed events.
+ if ((mask & Interop.Sys.NotifyEvents.IN_Q_OVERFLOW) != 0)
{
+ lock (s_watchersLock)
+ {
+ StopINotify();
+
+ foreach (var watcher in _watchers)
+ {
+ watcher.QueueError(CreateBufferOverflowException(watcher.BasePath));
+ }
+ }
return false;
}
- uint mask = nextEvent.mask;
-
- // 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)
+ // Renames come in the form of two events: IN_MOVED_FROM and IN_MOVED_TO.
+ // These should come as a sequence, one immediately after the other.
+ // This holds the directories from the previous event in case it was IN_MOVED_FROM.
+ ReadOnlySpan movedFromDirs = _dirBuffer.AsSpan(0, movedFromWatchCount);
+
+ // Look up the Watch in _wdToWatch.
+ // We take a writer lock to synchronize with AddOrUpdateWatchedDirectory and make sure newly added watch descriptors can be found in _wdToWatch.
+ _addLock.EnterWriteLock();
+ _addLock.ExitWriteLock();
+ _wdToWatch.TryGetValue(nextEvent.wd, out Watch? watch);
+
+ // Watches for this event.
+ ReadOnlySpan dirs = watch is not null ? GetWatchedDirectories(watch, ref _dirBuffer, offset: movedFromDirs.Length) : default;
+
+ // If the event after IN_MOVED_FROM is not a matching IN_MOVED_TO, we treat the IN_MOVED_FROM as a 'Deleted' in the next block.
+ // A matching IN_MOVED_TO will be handled as a 'Renamed' later on.
+ if (!movedFromDirs.IsEmpty)
{
- // 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)
+ bool isMatchingMovedTo = (mask & Interop.Sys.NotifyEvents.IN_MOVED_TO) != 0 && movedFromCookie == nextEvent.cookie;
+
+ foreach (var movedFrom in movedFromDirs)
{
- watcher.Restart();
+ bool isRename = isMatchingMovedTo && FindMatchingWatchedDirectory(dirs, movedFrom.Watcher) is not null;
+ if (isRename)
+ {
+ continue; // Handled as a Rename.
+ }
+
+ if (movedFromIsDir)
+ {
+ RemoveWatchedDirectoryChild(movedFrom, movedFromName);
+ }
+
+ var watcher = movedFrom.Watcher;
+ if (!IsIgnoredEvent(watcher, Interop.Sys.NotifyEvents.IN_DELETE, movedFromIsDir))
+ {
+ watcher.QueueEvent(WatcherEvent.Deleted(movedFrom, movedFromName));
+ }
+ }
+
+ if (!isMatchingMovedTo)
+ {
+ movedFromDirs = default;
}
- return false;
}
- // Look up the directory information for the supplied wd
- WatchedDirectory? associatedDirectoryEntry = null;
- lock (SyncObj)
+ // 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 & (Interop.Sys.NotifyEvents.IN_ISDIR | Interop.Sys.NotifyEvents.IN_IGNORED)) != 0;
+
+ // For IN_MOVED_FROM we check if there is an event pending that may be a matching IN_MOVED_TO.
+ // If there is, we defer the handling to the next ProcessEvent.
+ // If there isn't, we'll handle it as a 'Deleted' later on.
+ if ((mask & Interop.Sys.NotifyEvents.IN_MOVED_FROM) != 0)
{
- if (!_wdToPathMap.TryGetValue(nextEvent.wd, out associatedDirectoryEntry))
+ bool eventAvailable = _bufferPos != _bufferAvailable;
+ if (!eventAvailable)
+ {
+ // 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.PollEvents events;
+ Interop.Sys.Poll(_inotifyHandle, Interop.PollEvents.POLLIN, MillisecondsTimeout, out events);
+
+ eventAvailable = events != Interop.PollEvents.POLLNONE;
+ }
+ if (eventAvailable)
{
- // 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.
+ movedFromName = nextEvent.name;
+ dirs.CopyTo(_dirBuffer); // dirs won't be at the start of _dirBuffer when movedFromWatchCount was not zero.
+ movedFromWatchCount = dirs.Length;
+ movedFromCookie = nextEvent.cookie;
+ movedFromIsDir = isDir;
return true;
}
}
+ movedFromWatchCount = 0;
- ReadOnlySpan expandedName = associatedDirectoryEntry.GetPath(true, nextEvent.name);
-
- // To match Windows, ignore all changes that happen on the root folder itself
- if (expandedName.IsEmpty)
+ foreach (WatchedDirectory dir in dirs)
{
- return true;
- }
+ Watcher watcher = dir.Watcher;
- // 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;
+ WatchedDirectory? matchingFrom = (mask & Interop.Sys.NotifyEvents.IN_MOVED_TO) != 0 ? FindMatchingWatchedDirectory(movedFromDirs, watcher) : null;
- // 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.IsEmpty && ((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 (previousEventName.Equals(child.Name, StringComparison.Ordinal))
+ if (isDir && watcher.IncludeSubdirectories)
+ {
+ if ((mask & (Interop.Sys.NotifyEvents.IN_CREATE | Interop.Sys.NotifyEvents.IN_MOVED_TO)) != 0)
+ {
+ // If this is a rename, move over the watches from the source.
+ // We'll still call WatchChildDirectories in case the source was still being iterated for adding watches.
+ if (matchingFrom is not null)
{
- RemoveWatchedDirectory(child);
- return false;
+ RenameWatchedDirectories(dir, nextEvent.name, matchingFrom, movedFromName);
}
+
+ string directoryPath = dir.GetPath(nextEvent.name, pathBuffer, fullPath: true).ToString();
+ watcher.WatchChildDirectories(parent: dir, directoryPath);
+ }
+ else if ((mask & Interop.Sys.NotifyEvents.IN_MOVED_FROM) != 0)
+ {
+ RemoveWatchedDirectoryChild(dir, nextEvent.name);
}
}
+ // IN_IGNORED: Watch was removed explicitly or automatically because the directory was deleted.
+ if ((mask & Interop.Sys.NotifyEvents.IN_IGNORED) != 0)
+ {
+ RemoveWatchedDirectory(dir, ignoredFd: nextEvent.wd);
+ continue;
+ }
- // Then fire the deletion event, even though the event was IN_MOVED_FROM.
- watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, previousEventName);
+ // To match Windows, don't emit events for the root directory.
+ if (dir.IsRootDir && nextEvent.name.Length == 0)
+ {
+ continue;
+ }
- previousEventName = null;
- previousEventParent = null;
- previousEventCookie = 0;
- }
+ if (IsIgnoredEvent(watcher, mask, isDir))
+ {
+ continue;
+ }
- // 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);
+ switch (mask & EventMask)
+ {
+ case Interop.Sys.NotifyEvents.IN_CREATE:
+ watcher.QueueEvent(WatcherEvent.Created(dir, nextEvent.name));
+ break;
+ case Interop.Sys.NotifyEvents.IN_DELETE:
+ watcher.QueueEvent(WatcherEvent.Deleted(dir, nextEvent.name));
+ break;
+ case Interop.Sys.NotifyEvents.IN_ACCESS:
+ case Interop.Sys.NotifyEvents.IN_MODIFY:
+ case Interop.Sys.NotifyEvents.IN_ATTRIB:
+ watcher.QueueEvent(WatcherEvent.Changed(dir, nextEvent.name));
+ break;
+ case Interop.Sys.NotifyEvents.IN_MOVED_FROM:
+ watcher.QueueEvent(WatcherEvent.Deleted(dir, nextEvent.name));
+ break;
+ case Interop.Sys.NotifyEvents.IN_MOVED_TO:
+ if (matchingFrom is not null)
+ {
+ watcher.QueueEvent(WatcherEvent.Renamed(dir, nextEvent.name, matchingFrom, movedFromName));
+ }
+ else
+ {
+ watcher.QueueEvent(WatcherEvent.Created(dir, nextEvent.name));
+ }
+ break;
+ }
}
- // Check if the event should have been filtered but was unable because of inotify's inability
- // to filter files vs directories.
- const Interop.Sys.NotifyEvents fileDirEvents = Interop.Sys.NotifyEvents.IN_CREATE |
- Interop.Sys.NotifyEvents.IN_DELETE |
- Interop.Sys.NotifyEvents.IN_MOVED_FROM |
- Interop.Sys.NotifyEvents.IN_MOVED_TO;
- if ((((uint)fileDirEvents & mask) > 0) &&
- (isDir && ((_notifyFilters & NotifyFilters.DirectoryName) == 0) ||
- (!isDir && ((_notifyFilters & NotifyFilters.FileName) == 0))))
+ // For each Watcher we'll receive an IN_IGNORED for its root watch.
+ // If the root watch was found back as a WatchedDirectory via _wdToWatch above, then _allWatchersStopped will be updated by calling RemoveWatchedDirectory.
+ // If we didn't find back the WatchedDirectory, then RemoveWatchedDirectory was called already and it has updated _allWatchersStopped.
+ if (_allWatchersStopped)
{
- return true;
+ StopINotify();
+ return false;
}
- const Interop.Sys.NotifyEvents switchMask = fileDirEvents | Interop.Sys.NotifyEvents.IN_IGNORED |
- Interop.Sys.NotifyEvents.IN_ACCESS | Interop.Sys.NotifyEvents.IN_MODIFY | Interop.Sys.NotifyEvents.IN_ATTRIB;
- 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.PollEvents events;
- Interop.Sys.Poll(_inotifyHandle, Interop.PollEvents.POLLIN, MillisecondsTimeout, out events);
-
- // If we error or don't have any signaled handles, send the deleted event
- if (events == Interop.PollEvents.POLLNONE)
- {
- // There isn't any more data in the queue so this is a deleted event
- watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, expandedName);
- break;
- }
- }
+ return true;
- // 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;
+ void RemoveWatchedDirectoryChild(WatchedDirectory dir, string movedFromName)
+ {
+ Watcher watcher = dir.Watcher;
+ WatchedDirectory? child = null;
+ lock (watcher)
+ {
+ int idx = dir.FindChild(movedFromName);
+ if (idx != -1)
+ {
+ child = dir.Children![idx];
+ }
+ }
+ if (child is not null)
+ {
+ RemoveWatchedDirectory(child);
+ }
+ }
- break;
- case Interop.Sys.NotifyEvents.IN_MOVED_TO:
- if (!previousEventName.IsEmpty)
+ static ReadOnlySpan GetWatchedDirectories(Watch watch, ref WatchedDirectory[] buffer, int offset)
+ {
+ lock (watch)
+ {
+ int watchersCount = watch.Watchers.Count;
+ int lengthNeeded = watchersCount + offset;
+ if (lengthNeeded > buffer.Length)
{
- // If the previous name from IN_MOVED_FROM is non-empty, then this is a rename.
- watcher.NotifyRenameEventArgs(WatcherChangeTypes.Renamed, expandedName, previousEventName);
+ Array.Resize(ref buffer, lengthNeeded);
}
- else
+ watch.Watchers.CopyTo(buffer.AsSpan(offset));
+ return buffer.AsSpan(offset, watchersCount);
+ }
+ }
+
+ static WatchedDirectory? FindMatchingWatchedDirectory(ReadOnlySpan dir, Watcher watcher)
+ {
+ foreach (var d in dir)
+ {
+ if (d.Watcher == watcher)
{
- // 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);
+ return d;
}
- previousEventName = ReadOnlySpan.Empty;
- previousEventParent = null;
- previousEventCookie = 0;
- break;
+ }
+
+ return null;
}
- return true;
+ static bool IsIgnoredEvent(Watcher watcher, Interop.Sys.NotifyEvents mask, bool isDir)
+ {
+ return (watcher.WatchFilters & mask) == 0 ||
+ ((mask & FileDirEvents) != 0) &&
+ ((isDir && ((watcher.NotifyFilters & NotifyFilters.DirectoryName) == 0)) ||
+ (!isDir && ((watcher.NotifyFilters & NotifyFilters.FileName) == 0)));
+ }
}
- ///
- /// 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.
- ///
- private void ProcessEvents()
+ private void RenameWatchedDirectories(WatchedDirectory moveTo, string moveToName, WatchedDirectory moveFrom, string moveFromName)
{
- // 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.UnsafeRegister(obj => ((RunningInstance)obj!).CancellationCallback(), this);
- try
+ WatchedDirectory? sourceToRemove = null;
+
+ Watcher watcher = moveFrom.Watcher;
+ Debug.Assert(moveTo.Watcher == watcher);
+ lock (watcher)
{
- // Previous event information
- ReadOnlySpan previousEventName = ReadOnlySpan.Empty;
- WatchedDirectory? previousEventParent = null;
- uint previousEventCookie = 0;
+ int sourceIdx = moveFrom.FindChild(moveFromName);
+ if (sourceIdx == -1)
+ {
+ // unexpected: source not found.
+ return;
+ }
+ WatchedDirectory source = moveFrom.Children![sourceIdx];
- // Process events as long as we're not canceled and there are more to read...
- NotifyEvent nextEvent;
- while (!_cancellationToken.IsCancellationRequested && TryReadEvent(out nextEvent))
+ int dstIdx = moveTo.FindChild(moveToName);
+ if (dstIdx != -1)
{
- if (!ProcessEvent(nextEvent, ref previousEventName, ref previousEventParent, ref previousEventCookie))
- break;
+ // unexpected: the destination already exists. Leave it and stop watching the source.
+ sourceToRemove = source;
}
- }
- catch (Exception exc)
- {
- if (_weakWatcher.TryGetTarget(out FileSystemWatcher? watcher))
+ else
{
- watcher.OnError(new ErrorEventArgs(exc));
+ // We'll re-use the Watches.
+ moveFrom.Children.RemoveAt(sourceIdx);
+ WatchedDirectory renamed = CreateWatchedDirectoryFrom(moveTo, source, moveToName);
+ moveTo.InitializedChildren.Add(renamed);
}
}
- finally
+
+ if (sourceToRemove is not null)
{
- ctr.Dispose();
- _inotifyHandle.Dispose();
+ RemoveWatchedDirectory(sourceToRemove);
+ }
+
+ static WatchedDirectory CreateWatchedDirectoryFrom(WatchedDirectory parent, WatchedDirectory src, string name)
+ {
+ Watcher watcher = src.Watcher;
+ Debug.Assert(Monitor.IsEntered(watcher));
+
+ WatchedDirectory newDir;
+ Watch watch = src.Watch;
+ lock (watch)
+ {
+ newDir = new WatchedDirectory(watch, watcher, name, parent);
+ watch.Watchers.Remove(src);
+ watch.Watchers.Add(newDir);
+ }
+
+ if (src.Children is { } children)
+ {
+ foreach (var child in children)
+ {
+ newDir.InitializedChildren.Add(CreateWatchedDirectoryFrom(newDir, child, child.Name));
+ }
+ }
+
+ return newDir;
}
}
- /// Read event from the inotify handle into the supplied event object.
- /// The event object to be populated.
- /// if event was read successfully, otherwise.
private bool TryReadEvent(out NotifyEvent notifyEvent)
{
Debug.Assert(_buffer != null);
@@ -831,7 +862,7 @@ private bool TryReadEvent(out NotifyEvent notifyEvent)
notifyEvent = default(NotifyEvent);
return false;
}
- Debug.Assert(_bufferAvailable >= c_INotifyEventSize);
+ Debug.Assert(_bufferAvailable >= INotifyEventSize);
_bufferPos = 0;
}
@@ -843,14 +874,14 @@ private bool TryReadEvent(out NotifyEvent notifyEvent)
// uint32_t len;
// char name[]; // length determined by len; at least 1 for required null termination
// };
- Debug.Assert(_bufferPos + c_INotifyEventSize <= _bufferAvailable);
+ Debug.Assert(_bufferPos + INotifyEventSize <= _bufferAvailable);
NotifyEvent readEvent;
readEvent.wd = BitConverter.ToInt32(_buffer, _bufferPos);
readEvent.mask = BitConverter.ToUInt32(_buffer, _bufferPos + 4); // +4 to get past wd
readEvent.cookie = BitConverter.ToUInt32(_buffer, _bufferPos + 8); // +8 to get past wd, mask
int nameLength = (int)BitConverter.ToUInt32(_buffer, _bufferPos + 12); // +12 to get past wd, mask, cookie
- readEvent.name = ReadName(_bufferPos + c_INotifyEventSize, nameLength); // +16 to get past wd, mask, cookie, len
- _bufferPos += c_INotifyEventSize + nameLength;
+ readEvent.name = ReadName(_bufferPos + INotifyEventSize, nameLength); // +16 to get past wd, mask, cookie, len
+ _bufferPos += INotifyEventSize + nameLength;
notifyEvent = readEvent;
return true;
@@ -893,105 +924,489 @@ private struct NotifyEvent
internal string name;
}
- /// State associated with a watched directory.
- private sealed class WatchedDirectory
+ internal struct WatcherEvent
{
- /// A StringBuilder cached on the current thread to avoid allocations when possible.
- [ThreadStatic]
- private static StringBuilder? t_builder;
+ public const WatcherChangeTypes ErrorType = WatcherChangeTypes.All;
+
+ public string? Name { get; }
+ public WatchedDirectory? Directory { get; }
+ public string? OldName { get; }
+ public WatchedDirectory? OldDirectory { get; }
+ public Exception? Exception { get; }
+ public WatcherChangeTypes Type { get; }
+
+ private WatcherEvent(WatcherChangeTypes type, WatchedDirectory watch, string name, WatchedDirectory? oldWatch = null, string? oldName = null)
+ {
+ Type = type;
+ Directory = watch;
+ Name = name;
+ OldDirectory = oldWatch;
+ OldName = oldName;
+ }
+
+ private WatcherEvent(Exception exception)
+ {
+ Type = ErrorType;
+ Exception = exception;
+ }
- /// The parent directory.
- internal WatchedDirectory? Parent;
+ public static WatcherEvent Deleted(WatchedDirectory dir, string name)
+ => new WatcherEvent(WatcherChangeTypes.Deleted, dir, name);
- /// The watch descriptor associated with this directory.
- internal int WatchDescriptor;
+ public static WatcherEvent Created(WatchedDirectory dir, string name)
+ => new WatcherEvent(WatcherChangeTypes.Created, dir, name);
- /// The filename of this directory.
- internal string? Name;
+ public static WatcherEvent Changed(WatchedDirectory dir, string name)
+ => new WatcherEvent(WatcherChangeTypes.Changed, dir, name);
- /// Child directories of this directory for which we added explicit watches.
- internal List? Children;
+ public static WatcherEvent Renamed(WatchedDirectory dir, string name, WatchedDirectory oldDir, string oldName)
+ => new WatcherEvent(WatcherChangeTypes.Renamed, dir, name, oldDir, oldName);
- /// Child directories of this directory for which we added explicit watches. This is the same as Children, but ensured to be initialized as non-null.
- internal List InitializedChildren => Children ??= new List();
+ public static WatcherEvent Error(Exception exception)
+ => new WatcherEvent(exception);
- // PERF: Work is being done here proportionate to depth of watch directories.
- // If this becomes a bottleneck, we'll need to come up with another mechanism
- // for obtaining and keeping paths up to date, for example storing the full path
- // in each WatchedDirectory node and recursively updating all children on a move,
- // which we can do given that we store all children. For now we're not doing that
- // because it's not a clear win: either you update all children recursively when
- // a directory moves / is added, or you compute each name when an event occurs.
- // The former is better if there are going to be lots of events causing lots
- // of traversals to compute names, and the latter is better for big directory
- // structures that incur fewer file events.
+ public ReadOnlySpan GetName(Span pathBuffer)
+ => Directory!.GetPath(Name, pathBuffer);
- /// Gets the path of this directory.
- /// Whether to get a path relative to the root directory being watched, or a full path.
- /// An additional name to include in the path, relative to this directory.
- /// The computed path.
- internal string GetPath(bool relativeToRoot, string? additionalName = null)
+ public ReadOnlySpan GetOldName(Span pathBuffer)
+ => OldDirectory!.GetPath(OldName, pathBuffer);
+ }
+
+ public sealed class Watcher
+ {
+ // Ignore links.
+ private static readonly EnumerationOptions ChildEnumerationOptions =
+ new() { RecurseSubdirectories = false, MatchType = MatchType.Win32, AttributesToSkip = FileAttributes.ReparsePoint, IgnoreInaccessible = false };
+
+ ///
+ /// Weak reference to the associated watcher. A weak reference is used so that the FileSystemWatcher may be collected and finalized,
+ /// causing an active operation to be torn down. With a strong reference, a blocking read on the inotify handle will keep alive this
+ /// instance which will keep alive the FileSystemWatcher which will not be finalizable and thus which will never signal to the blocking
+ /// read to wake up in the event that the user neglects to stop raising events.
+ ///
+ private readonly WeakReference _weakFsw;
+ private readonly Channel _eventQueue;
+ private INotify? _inotify;
+ private bool _emitEvents;
+ private WatchedDirectory? _rootDirectory;
+
+ public string BasePath { get; }
+ public NotifyFilters NotifyFilters { get; }
+ public Interop.Sys.NotifyEvents WatchFilters { get; }
+ public bool IncludeSubdirectories { get; }
+ public bool IsStopped { get; set; }
+
+ public WatchedDirectory? RootDirectory
{
- // Use our cached builder
- StringBuilder builder = (t_builder ??= new StringBuilder());
- builder.Clear();
+ get
+ {
+ Debug.Assert(Monitor.IsEntered(this));
+ return _rootDirectory;
+ }
+ set
+ {
+ Debug.Assert(Monitor.IsEntered(this));
+ _rootDirectory = value;
+ }
+ }
- // Write the directory's path. Then if an additional filename was supplied, append it
- Write(builder, relativeToRoot);
- if (additionalName != null)
+ public Watcher(FileSystemWatcher fsw)
+ {
+ _weakFsw = new WeakReference(fsw);
+ BasePath = System.IO.Path.TrimEndingDirectorySeparator(System.IO.Path.GetFullPath(fsw.Path));
+ IncludeSubdirectories = fsw.IncludeSubdirectories;
+ NotifyFilters = fsw.NotifyFilter;
+ WatchFilters = TranslateFilters(NotifyFilters);
+
+ // This channel is unbounded which means that if the FileSystemWatcher event handlers can't keep up, the queue size will increase and consume memory.
+ // We could implement a bound that is based on the FileSystemWatcher.InternalBufferSize property.
+ _eventQueue = Channel.CreateUnbounded(new UnboundedChannelOptions() { AllowSynchronousContinuations = false, SingleReader = true });
+ }
+
+ public void Start()
+ {
+ Debug.Assert(_inotify is null);
+
+ INotify? inotify;
+ WatchedDirectory? rootDirectory;
+ lock (s_watchersLock)
{
- AppendSeparatorIfNeeded(builder);
- builder.Append(additionalName);
+ inotify = s_currentInotify;
+ // If there is no running instance, start one.
+ if (inotify is null || inotify.IsStopped)
+ {
+ inotify = new();
+ inotify.StartThread();
+ s_currentInotify = inotify;
+ }
+
+ _inotify = inotify;
+
+ rootDirectory = CreateRootWatch();
+ if (rootDirectory is not null)
+ {
+ _inotify.AddWatcher(this);
+ }
}
- return builder.ToString();
+
+ _ = DequeueEvents();
+
+ if (rootDirectory is not null && IncludeSubdirectories)
+ {
+ if (IncludeSubdirectories)
+ {
+ WatchChildDirectories(rootDirectory, BasePath, includeBasePath: false);
+ }
+ }
+
+ _emitEvents = true;
+
+ WatchedDirectory? CreateRootWatch()
+ => _inotify.AddOrUpdateWatchedDirectory(this, parent: null, BasePath, WatchFilters, followLinks: true, ignoreMissing: false);
}
- /// Write's this directory's path to the builder.
- /// The builder to which to write.
- ///
- /// true if the path should be relative to the root directory being watched.
- /// false if the path should be a full file system path, including that of
- /// the root directory being watched.
- ///
- private void Write(StringBuilder builder, bool relativeToRoot)
+ public void Stop()
{
- // 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.
+ WatchedDirectory? root;
+ lock (this)
+ {
+ if (IsStopped)
+ {
+ return;
+ }
+ IsStopped = true;
+ _emitEvents = false;
- // First append the parent's path
- if (Parent != null)
+ root = RootDirectory;
+ }
+
+ _eventQueue.Writer.Complete();
+
+ if (root is not null)
{
- Parent.Write(builder, relativeToRoot);
- AppendSeparatorIfNeeded(builder);
+ Debug.Assert(_inotify is not null);
+ _inotify.RemoveWatchedDirectory(root);
}
+ }
- // Then append ours. In the case of the root directory
- // being watched, we only append its name if the caller
- // has asked for a full path.
- if (Parent != null || !relativeToRoot)
+ private async Task DequeueEvents()
+ {
+ char[] pathBuffer = new char[PATH_MAX];
+ try
+ {
+ await foreach (WatcherEvent evnt in _eventQueue.Reader.ReadAllAsync().ConfigureAwait(false))
+ {
+ if (IsStopped)
+ {
+ break;
+ }
+ EmitEvent(evnt, pathBuffer);
+ }
+ }
+ catch (Exception ex)
{
- builder.Append(Name);
+ if (!IsStopped)
+ {
+ Stop();
+
+ try
+ {
+ Fsw?.OnError(new ErrorEventArgs(ex));
+ }
+ catch
+ { }
+ }
}
}
- /// Adds a directory path separator to the end of the builder if one isn't there.
- /// The builder.
- private static void AppendSeparatorIfNeeded(StringBuilder builder)
+ private void EmitEvent(WatcherEvent evnt, char[] pathBuffer)
{
- if (builder.Length > 0)
+ FileSystemWatcher? fsw = Fsw;
+ if (fsw is null)
+ {
+ return;
+ }
+
+ switch (evnt.Type)
{
- char c = builder[builder.Length - 1];
- if (c != System.IO.Path.DirectorySeparatorChar && c != System.IO.Path.AltDirectorySeparatorChar)
+ case WatcherEvent.ErrorType:
+ fsw.OnError(new ErrorEventArgs(evnt.Exception!));
+
+ // On InternalBufferOverflowException, the inotify is stopped.
+ // If the Watcher wasn't stopped, Restart it against a new inotify instance.
+ if (evnt.Exception is InternalBufferOverflowException)
+ {
+ if (!IsStopped)
+ {
+ fsw.Restart();
+ }
+ }
+
+ break;
+ case WatcherChangeTypes.Created:
+ case WatcherChangeTypes.Deleted:
+ case WatcherChangeTypes.Changed:
+ {
+ ReadOnlySpan name = evnt.GetName(pathBuffer);
+ fsw.NotifyFileSystemEventArgs(evnt.Type, name);
+ }
+ break;
+ case WatcherChangeTypes.Renamed:
+ {
+ string name = evnt.GetName(pathBuffer).ToString();
+ ReadOnlySpan oldName = evnt.GetOldName(pathBuffer);
+ fsw.NotifyRenameEventArgs(WatcherChangeTypes.Renamed, name, oldName);
+ }
+ break;
+ }
+ }
+
+ internal bool WatchChildDirectories(WatchedDirectory parent, string path, bool includeBasePath = true)
+ {
+ Debug.Assert(_inotify is not null);
+ if (IsStopped)
+ {
+ return false;
+ }
+
+ if (includeBasePath)
+ {
+ WatchedDirectory? newParent = AddOrUpdateWatch(parent, path);
+ if (newParent is null)
+ {
+ // We couldn't recurse this path, but we should continue to try the others.
+ return true;
+ }
+ parent = newParent;
+ }
+
+ try
+ {
+ foreach (var childDir in Directory.GetDirectories(path, "*", ChildEnumerationOptions))
+ {
+ if (!WatchChildDirectories(parent, childDir))
+ {
+ return false;
+ }
+ }
+ }
+ catch (DirectoryNotFoundException)
+ { } // path was removed
+ catch (IOException ex) when (ex.HResult == Interop.Error.ENOTDIR.Info().RawErrno)
+ { } // path was replaced by a file.
+ catch (Exception ex)
+ {
+ QueueError(ex);
+ }
+
+ return true;
+
+ WatchedDirectory? AddOrUpdateWatch(WatchedDirectory parent, string path)
+ => _inotify.AddOrUpdateWatchedDirectory(this, parent, path, WatchFilters, followLinks: false, ignoreMissing: true);
+ }
+
+ internal void QueueEvent(WatcherEvent ev)
+ {
+ Debug.Assert(ev.Type != WatcherEvent.ErrorType);
+ if (!_emitEvents)
+ {
+ return;
+ }
+ _eventQueue.Writer.TryWrite(ev);
+ }
+
+ internal void QueueError(Exception exception)
+ {
+ if (IsStopped)
+ {
+ return;
+ }
+ _eventQueue.Writer.TryWrite(WatcherEvent.Error(exception));
+ }
+
+ private FileSystemWatcher? Fsw
+ {
+ get
+ {
+ _weakFsw.TryGetTarget(out FileSystemWatcher? watcher);
+ return watcher;
+ }
+ }
+
+ ///
+ /// Maps the FileSystemWatcher's NotifyFilters enumeration to the
+ /// corresponding Interop.Sys.NotifyEvents values.
+ ///
+ /// The filters provided the by user.
+ /// The corresponding NotifyEvents values to use with inotify.
+ private static Interop.Sys.NotifyEvents TranslateFilters(NotifyFilters filters)
+ {
+ Interop.Sys.NotifyEvents result = 0;
+
+ // 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.Sys.NotifyEvents.IN_CREATE |
+ Interop.Sys.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.Sys.NotifyEvents.IN_ACCESS;
+ }
+ if ((filters & filtersForModify) != 0)
+ {
+ result |= Interop.Sys.NotifyEvents.IN_MODIFY;
+ }
+ if ((filters & filtersForAttrib) != 0)
+ {
+ result |= Interop.Sys.NotifyEvents.IN_ATTRIB;
+ }
+
+ // For the Rename event, we'll register for the corresponding move inotify events if the
+ // caller's NotifyFilters asks for notifications related to names.
+ const NotifyFilters filtersForMoved =
+ NotifyFilters.FileName |
+ NotifyFilters.DirectoryName;
+ if ((filters & filtersForMoved) != 0)
+ {
+ result |=
+ Interop.Sys.NotifyEvents.IN_MOVED_FROM |
+ Interop.Sys.NotifyEvents.IN_MOVED_TO;
+ }
+
+ return result;
+ }
+ }
+
+ internal sealed class WatchedDirectory
+ {
+ private List? _children;
+
+ public Watch Watch { get; }
+ public Watcher Watcher { get; }
+ public string Name { get; }
+ public WatchedDirectory? Parent { get; }
+ public bool IsRootDir => Parent is null;
+
+ public WatchedDirectory(Watch watch, Watcher watcher, string name, WatchedDirectory? parent)
+ {
+ Watch = watch;
+ Watcher = watcher;
+ Name = name;
+ Parent = parent;
+ }
+
+ public List? Children
+ {
+ get
+ {
+ Debug.Assert(Monitor.IsEntered(Watcher));
+ return _children;
+ }
+ set
+ {
+ Debug.Assert(Monitor.IsEntered(Watcher));
+ _children = value;
+ }
+ }
+ public List InitializedChildren => Children ??= new List();
+
+ public int FindChild(string name)
+ {
+ Debug.Assert(Monitor.IsEntered(Watcher));
+ var children = Children;
+ if (children is null)
+ {
+ return -1;
+ }
+ for (int i = 0; i < children.Count; i++)
+ {
+ if (children[i].Name == name)
{
- builder.Append(System.IO.Path.DirectorySeparatorChar);
+ return i;
}
}
+ return -1;
+ }
+
+ internal ReadOnlySpan GetPath(ReadOnlySpan childName, Span pathBuffer, bool fullPath = false)
+ {
+ int length = 0;
+
+ if (Parent is not null)
+ {
+ length = Parent.GetPath("", pathBuffer, fullPath).Length;
+ fullPath = false;
+ }
+
+ if (fullPath)
+ {
+ Append(pathBuffer, Watcher.BasePath);
+ }
+
+ Append(pathBuffer, Name);
+ Append(pathBuffer, childName);
+
+ return pathBuffer.Slice(0, length);
+
+ void Append(Span pathBuffer, ReadOnlySpan path)
+ {
+ if (path.Length == 0)
+ {
+ return;
+ }
+
+ if (length != 0 && pathBuffer[length - 1] != '/')
+ {
+ pathBuffer[length] = '/';
+ length++;
+ }
+
+ path.CopyTo(pathBuffer.Slice(length));
+ length += path.Length;
+ }
}
}
- }
+ internal sealed class Watch
+ {
+ private List _watchers = new();
+
+ public int WatchDescriptor { get; }
+ public List Watchers
+ {
+ get
+ {
+ Debug.Assert(Monitor.IsEntered(this));
+ return _watchers;
+ }
+ }
+
+ public Watch(int watchDescriptor)
+ {
+ WatchDescriptor = watchDescriptor;
+ }
+ }
+ }
}
}
diff --git a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs
index f285b0572ccd4d..ceceb6289006f9 100644
--- a/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs
+++ b/src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs
@@ -397,11 +397,13 @@ private void NotifyInternalBufferOverflowEvent()
{
if (_onErrorHandler != null)
{
- OnError(new ErrorEventArgs(
- new InternalBufferOverflowException(SR.Format(SR.FSW_BufferOverflow, _directory))));
+ OnError(new ErrorEventArgs(CreateBufferOverflowException(_directory)));
}
}
+ private static InternalBufferOverflowException CreateBufferOverflowException(string directoryPath)
+ => new InternalBufferOverflowException(SR.Format(SR.FSW_BufferOverflow, directoryPath));
+
///
/// Raises the event to each handler in the list.
///