|
// 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.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
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.
public partial class FileSystemWatcher
{
/// <summary>Starts a new watch operation if one is not currently running.</summary>
private void StartRaisingEvents()
{
// If we're called when "Initializing" is true, set enabled to true
if (IsSuspended())
{
_enabled = true;
return;
}
// If we already have a cancellation object, we're already running.
if (_cancellation != 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;
// 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;
}
}
/// <summary>Cancels the currently running watch operation if there is one.</summary>
private void StopRaisingEvents()
{
_enabled = false;
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();
}
}
/// <summary>Called when FileSystemWatcher is finalized.</summary>
private void FinalizeDispose()
{
// The RunningInstance 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();
}
/// <summary>Path to the procfs file that contains the maximum number of inotify instances an individual user may create.</summary>
private const string MaxUserInstancesPath = "/proc/sys/fs/inotify/max_user_instances";
/// <summary>Path to the procfs file that contains the maximum number of inotify watches an individual user may create.</summary>
private const string MaxUserWatchesPath = "/proc/sys/fs/inotify/max_user_watches";
/// <summary>
/// Cancellation for the currently running watch operation.
/// This is non-null if an operation has been started and null if stopped.
/// </summary>
private CancellationTokenSource? _cancellation;
/// <summary>Reads the value of a max user limit path from procfs.</summary>
/// <param name="path">The path to read.</param>
/// <returns>The value read, or "0" if a failure occurred.</returns>
private static string? ReadMaxUserLimit(string path)
{
try { return File.ReadAllText(path).Trim(); }
catch { return null; }
}
/// <summary>
/// Maps the FileSystemWatcher's NotifyFilters enumeration to the
/// corresponding Interop.Sys.NotifyEvents values.
/// </summary>
/// <param name="filters">The filters provided the by user.</param>
/// <returns>The corresponding NotifyEvents values to use with inotify.</returns>
private static Interop.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;
}
/// <summary>
/// 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.
/// </summary>
private sealed class RunningInstance
{
/// <summary>
/// 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.
/// </summary>
private const int c_INotifyEventSize = 16;
/// <summary>
/// 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.
/// </summary>
private readonly WeakReference<FileSystemWatcher> _weakWatcher;
/// <summary>
/// The path for the primary watched directory.
/// </summary>
private readonly string _directoryPath;
/// <summary>
/// The inotify handle / file descriptor
/// </summary>
private readonly SafeFileHandle _inotifyHandle;
/// <summary>
/// Buffer used to store raw bytes read from the inotify handle.
/// </summary>
private readonly byte[] _buffer;
/// <summary>
/// The number of bytes read into the _buffer.
/// </summary>
private int _bufferAvailable;
/// <summary>
/// The next position in _buffer from which an event should be read.
/// </summary>
private int _bufferPos;
/// <summary>
/// Filters to use when adding a watch on directories.
/// </summary>
private readonly NotifyFilters _notifyFilters;
private readonly Interop.Sys.NotifyEvents _watchFilters;
/// <summary>
/// Whether to monitor subdirectories. Unlike Win32, inotify does not implicitly monitor subdirectories;
/// watches must be explicitly added for those subdirectories.
/// </summary>
private readonly bool _includeSubdirectories;
/// <summary>
/// Token to monitor for cancellation requests, upon which processing is stopped and all
/// state is cleaned up.
/// </summary>
private readonly CancellationToken _cancellationToken;
/// <summary>
/// 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.
/// </summary>
private readonly Dictionary<int, WatchedDirectory> _wdToPathMap = new Dictionary<int, WatchedDirectory>();
/// <summary>
/// Maximum length of a name returned from inotify event.
/// </summary>
private const int NAME_MAX = 255; // from limits.h
/// <summary>Initializes the instance with all state necessary to operate a watch.</summary>
internal RunningInstance(
FileSystemWatcher watcher, SafeFileHandle inotifyHandle, string directoryPath,
bool includeSubdirectories, NotifyFilters notifyFilters, CancellationToken cancellationToken)
{
Debug.Assert(watcher != null);
Debug.Assert(inotifyHandle != null && !inotifyHandle.IsInvalid && !inotifyHandle.IsClosed);
Debug.Assert(directoryPath != null);
_weakWatcher = new WeakReference<FileSystemWatcher>(watcher);
_inotifyHandle = inotifyHandle;
_directoryPath = directoryPath;
_buffer = watcher.AllocateBuffer();
Debug.Assert(_buffer != null && _buffer.Length > (c_INotifyEventSize + NAME_MAX + 1));
_includeSubdirectories = includeSubdirectories;
_notifyFilters = notifyFilters;
_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);
}
internal void Start()
{
// Spawn a thread to read from the inotify queue and process the events.
new Thread(obj => ((RunningInstance)obj!).ProcessEvents())
{
IsBackground = true,
Name = ".NET File Watcher"
}.Start(this);
// PERF: As needed, we can look into making this use async I/O rather than burning
// a thread that blocks in the read syscall.
}
/// <summary>Object to use for synchronizing access to state when necessary.</summary>
private object SyncObj { get { return _wdToPathMap; } }
/// <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 (_wdToPathMap.Count > 0)
{
AddDirectoryWatchUnlocked(parent, directoryName);
}
}
}
/// <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)
{
bool hasParent = parent != null;
string fullPath = hasParent ? parent!.GetPath(false, directoryName) : directoryName;
// 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;
}
// 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 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 (hasParent && (error.Error == Interop.Error.ENOENT ||
error.Error == Interop.Error.ENOTDIR))
{
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
{
exc = Interop.GetExceptionForIoErrno(error, fullPath);
}
if (_weakWatcher.TryGetTarget(out FileSystemWatcher? 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)
{
// Work around https://github.com/dotnet/csharplang/issues/3393 preventing Parent?.Children!. from behaving as expected
if (directoryEntry.Parent != null)
{
directoryEntry.Parent.Children!.Remove(directoryEntry);
}
directoryEntry.Parent = parent;
if (hasParent)
{
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 (hasParent)
{
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)
{
try
{
// 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.
}
}
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)
{
if (_weakWatcher.TryGetTarget(out FileSystemWatcher? watcher))
{
watcher.OnError(new ErrorEventArgs(ex));
}
}
}
}
/// <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)
{
// Work around https://github.com/dotnet/csharplang/issues/3393 preventing Parent?.Children!. from behaving as expected
if (directoryEntry.Parent != null)
{
directoryEntry.Parent.Children!.Remove(directoryEntry);
}
RemoveWatchedDirectoryUnlocked(directoryEntry, removeInotify);
}
}
/// <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)
{
// 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);
}
}
/// <summary>
/// Callback invoked when cancellation is requested. Removes all watches,
/// which will cause the active processing loop to shutdown.
/// </summary>
private void CancellationCallback()
{
lock (SyncObj)
{
// 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)
{
int result = Interop.Sys.INotifyRemoveWatch(_inotifyHandle, wd);
Debug.Assert(result >= 0); // ignore errors; they're non-fatal, but they also shouldn't happen
}
_wdToPathMap.Clear();
}
}
/// <summary>
/// Processes the next event. Method does not inline to prevent a strong reference to the watcher.
/// </summary>
/// <param name="nextEvent">The next event.</param>
/// <param name="previousEventName">The previous event's name.</param>
/// <param name="previousEventParent">The previous event's parent.</param>
/// <param name="previousEventCookie">The previous event's cookie.</param>
/// <returns><see langword="true"/> if we can continue processing events, <see langword="false"/> otherwise.</returns>
[MethodImpl(MethodImplOptions.NoInlining)]
private bool ProcessEvent(NotifyEvent nextEvent, ref ReadOnlySpan<char> previousEventName, ref WatchedDirectory? previousEventParent, ref uint previousEventCookie)
{
// 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))
{
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)
{
// Notify the caller of the error and, if the includeSubdirectories flag is set, restart to pick up any
// potential directories we missed due to the overflow.
watcher.NotifyInternalBufferOverflowEvent();
if (_includeSubdirectories)
{
watcher.Restart();
}
return false;
}
// Look up the directory information for the supplied wd
WatchedDirectory? associatedDirectoryEntry = null;
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.
return true;
}
}
ReadOnlySpan<char> expandedName = associatedDirectoryEntry.GetPath(true, nextEvent.name);
// To match Windows, ignore all changes that happen on the root folder itself
if (expandedName.IsEmpty)
{
return true;
}
// Determine whether the affected object is a directory (rather than a file).
// If it is, we may need to do special processing, such as adding a watch for new
// directories if IncludeSubdirectories is enabled. Since we're only watching
// directories, any IN_IGNORED event is also for a directory.
bool isDir = (mask & (uint)(Interop.Sys.NotifyEvents.IN_ISDIR | Interop.Sys.NotifyEvents.IN_IGNORED)) != 0;
// Renames come in the form of two events: IN_MOVED_FROM and IN_MOVED_TO.
// In general, these should come as a sequence, one immediately after the other.
// So, we delay raising an event for IN_MOVED_FROM until we see what comes next.
if (!previousEventName.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))
{
RemoveWatchedDirectory(child);
return false;
}
}
}
// Then fire the deletion event, even though the event was IN_MOVED_FROM.
watcher.NotifyFileSystemEventArgs(WatcherChangeTypes.Deleted, previousEventName);
previousEventName = null;
previousEventParent = null;
previousEventCookie = 0;
}
// If the event signaled that there's a new subdirectory and if we're monitoring subdirectories,
// add a watch for it.
const Interop.Sys.NotifyEvents AddMaskFilters = Interop.Sys.NotifyEvents.IN_CREATE | Interop.Sys.NotifyEvents.IN_MOVED_TO;
bool addWatch = ((mask & (uint)AddMaskFilters) != 0);
if (addWatch && isDir && _includeSubdirectories)
{
AddDirectoryWatch(associatedDirectoryEntry, nextEvent.name);
}
// 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))))
{
return true;
}
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;
}
}
// We will set these values if the buffer has more data OR if the poll call tells us that more data is available.
previousEventName = expandedName;
previousEventParent = isDir ? associatedDirectoryEntry : null;
previousEventCookie = nextEvent.cookie;
break;
case Interop.Sys.NotifyEvents.IN_MOVED_TO:
if (!previousEventName.IsEmpty)
{
// If the previous name from IN_MOVED_FROM is non-empty, 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 = ReadOnlySpan<char>.Empty;
previousEventParent = null;
previousEventCookie = 0;
break;
}
return true;
}
/// <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.UnsafeRegister(obj => ((RunningInstance)obj!).CancellationCallback(), this);
try
{
// Previous event information
ReadOnlySpan<char> previousEventName = ReadOnlySpan<char>.Empty;
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))
{
if (!ProcessEvent(nextEvent, ref previousEventName, ref previousEventParent, ref previousEventCookie))
break;
}
}
catch (Exception exc)
{
if (_weakWatcher.TryGetTarget(out FileSystemWatcher? watcher))
{
watcher.OnError(new ErrorEventArgs(exc));
}
}
finally
{
ctr.Dispose();
_inotifyHandle.Dispose();
}
}
/// <summary>Read event from the inotify handle into the supplied event object.</summary>
/// <param name="notifyEvent">The event object to be populated.</param>
/// <returns><see langword="true"/> if event was read successfully, <see langword="false"/> otherwise.</returns>
private bool TryReadEvent(out NotifyEvent notifyEvent)
{
Debug.Assert(_buffer != null);
Debug.Assert(_buffer.Length > 0);
Debug.Assert(_bufferAvailable >= 0 && _bufferAvailable <= _buffer.Length);
Debug.Assert(_bufferPos >= 0 && _bufferPos <= _bufferAvailable);
// Read more data into our buffer if we need it
if (_bufferAvailable == 0 || _bufferPos == _bufferAvailable)
{
// Read from the handle. This will block until either data is available
// or all watches have been removed, in which case zero bytes are read.
unsafe
{
try
{
fixed (byte* buf = &_buffer[0])
{
_bufferAvailable = Interop.CheckIo(Interop.Sys.Read(_inotifyHandle, buf, this._buffer.Length));
Debug.Assert(_bufferAvailable <= this._buffer.Length);
}
}
catch (ArgumentException)
{
_bufferAvailable = 0;
Debug.Fail("Buffer provided to read was too small");
}
Debug.Assert(_bufferAvailable >= 0);
}
if (_bufferAvailable == 0)
{
notifyEvent = default(NotifyEvent);
return false;
}
Debug.Assert(_bufferAvailable >= c_INotifyEventSize);
_bufferPos = 0;
}
// Parse each event:
// struct inotify_event {
// int wd;
// uint32_t mask;
// uint32_t cookie;
// uint32_t len;
// char name[]; // length determined by len; at least 1 for required null termination
// };
Debug.Assert(_bufferPos + c_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;
notifyEvent = readEvent;
return true;
}
/// <summary>
/// Reads a UTF-8 string from _buffer starting at the specified position and up to
/// the specified length. Null termination is trimmed off (the length may include
/// many null bytes, not just one, or it may include none).
/// </summary>
/// <param name="position"></param>
/// <param name="nameLength"></param>
/// <returns></returns>
private string ReadName(int position, int nameLength)
{
Debug.Assert(position > 0);
Debug.Assert(nameLength >= 0 && (position + nameLength) <= _buffer.Length);
int lengthWithoutNullTerm = _buffer.AsSpan(position, nameLength).IndexOf((byte)'\0');
if (lengthWithoutNullTerm < 0)
{
lengthWithoutNullTerm = nameLength;
}
return lengthWithoutNullTerm > 0 ?
Encoding.UTF8.GetString(_buffer, position, lengthWithoutNullTerm) :
string.Empty;
}
/// <summary>An event read and translated from the inotify handle.</summary>
/// <remarks>
/// Unlike it's native counterpart, this struct stores a string name rather than
/// an integer length and a char[]. It is not directly marshalable.
/// </remarks>
private struct NotifyEvent
{
internal int wd;
internal uint mask;
internal uint cookie;
internal string name;
}
/// <summary>State associated with a watched directory.</summary>
private sealed class WatchedDirectory
{
/// <summary>A StringBuilder cached on the current thread to avoid allocations when possible.</summary>
[ThreadStatic]
private static StringBuilder? t_builder;
/// <summary>The parent directory.</summary>
internal WatchedDirectory? Parent;
/// <summary>The watch descriptor associated with this directory.</summary>
internal int WatchDescriptor;
/// <summary>The filename of this directory.</summary>
internal string? Name;
/// <summary>Child directories of this directory for which we added explicit watches.</summary>
internal List<WatchedDirectory>? Children;
/// <summary>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.</summary>
internal List<WatchedDirectory> InitializedChildren => Children ??= new List<WatchedDirectory>();
// 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.
/// <summary>Gets the path of this directory.</summary>
/// <param name="relativeToRoot">Whether to get a path relative to the root directory being watched, or a full path.</param>
/// <param name="additionalName">An additional name to include in the path, relative to this directory.</param>
/// <returns>The computed path.</returns>
internal string GetPath(bool relativeToRoot, string? additionalName = null)
{
// Use our cached builder
StringBuilder builder = (t_builder ??= new StringBuilder());
builder.Clear();
// Write the directory's path. Then if an additional filename was supplied, append it
Write(builder, relativeToRoot);
if (additionalName != null)
{
AppendSeparatorIfNeeded(builder);
builder.Append(additionalName);
}
return builder.ToString();
}
/// <summary>Write's this directory's path to the builder.</summary>
/// <param name="builder">The builder to which to write.</param>
/// <param name="relativeToRoot">
/// 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.
/// </param>
private void Write(StringBuilder builder, bool relativeToRoot)
{
// 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.
// First append the parent's path
if (Parent != null)
{
Parent.Write(builder, relativeToRoot);
AppendSeparatorIfNeeded(builder);
}
// 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)
{
builder.Append(Name);
}
}
/// <summary>Adds a directory path separator to the end of the builder if one isn't there.</summary>
/// <param name="builder">The builder.</param>
private static void AppendSeparatorIfNeeded(StringBuilder builder)
{
if (builder.Length > 0)
{
char c = builder[builder.Length - 1];
if (c != System.IO.Path.DirectorySeparatorChar && c != System.IO.Path.AltDirectorySeparatorChar)
{
builder.Append(System.IO.Path.DirectorySeparatorChar);
}
}
}
}
}
}
}
|