|
// 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
{
// 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;
/// <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 watcher object, we're already running.
if (_watcher != null)
{
return;
}
_watcher = new INotify.Watcher(this);
_enabled = true;
_watcher.Start();
}
/// <summary>Cancels the currently running watch operation if there is one.</summary>
private void StopRaisingEvents()
{
_enabled = false;
if (IsSuspended())
return;
_watcher?.Stop();
_watcher = null;
}
/// <summary>Called when FileSystemWatcher is finalized.</summary>
private void FinalizeDispose()
{
// 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();
}
/// <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";
private INotify.Watcher? _watcher;
private static void RestartForInternalBufferSize()
{
// The implementation is not using InternalBufferSize. There's no need to restart.
}
/// <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; }
}
private sealed class INotify
{
/// <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 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 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<Watcher> _watchers = new();
private readonly byte[] _buffer = new byte[BufferSize];
private readonly SafeFileHandle _inotifyHandle;
private readonly ConcurrentDictionary<int, Watch> _wdToWatch = new ConcurrentDictionary<int, Watch>();
private readonly ReaderWriterLockSlim _addLock = new(LockRecursionPolicy.NoRecursion);
private bool _isThreadStopping;
private bool _allWatchersStopped;
private int _bufferAvailable;
private int _bufferPos;
private WatchedDirectory[] _dirBuffer = new WatchedDirectory[4];
public INotify()
{
_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);
}
private void StartThread()
{
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
{
StopINotify();
throw;
}
}
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);
// Sync with AddOrUpdateWatchedDirectory and RemoveUnusedINotifyWatches to stop using the inotify handle.
_addLock.EnterWriteLock();
_isThreadStopping = true;
_addLock.ExitWriteLock();
// Close the handle.
_inotifyHandle.Dispose();
}
private WatchedDirectory? AddOrUpdateWatchedDirectory(Watcher watcher, WatchedDirectory? parent, string directoryPath, Interop.Sys.NotifyEvents watchFilters, bool ignoreMissing = true)
{
Debug.Assert(!Monitor.IsEntered(watcher)); // We mustn'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
{
// Serialize adding watches to the same watcher.
// Concurrently adding watches may happen during the initial recursive iteration of the directory.
// This ensures the WatchedDirectory matches with the most recent INotifyAddWatch directory.
lock (watcher)
{
if (_isThreadStopping // inotify thread stopping
|| watcher.IsWatcherStopped // 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
(parent == null ? 0 : Interop.Sys.NotifyEvents.IN_DONT_FOLLOW); // Follow links only for the root path, not the subdirs.
// 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;
// Track when directories are added/removed.
if (watcher.IncludeSubdirectories)
{
mask |= 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 watchDescriptor) => new Watch(watchDescriptor), (int watchDescriptor, 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 which was moved/deleted, 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;
}
private void RemoveWatchedDirectory(WatchedDirectory dir, int ignoredFd = -1)
{
bool removeINotifyWatches = false;
RemoveWatchedDirectoryFromParentAndWatches(dir, ref removeINotifyWatches);
if (removeINotifyWatches)
{
RemoveUnusedINotifyWatches(dir, ignoredFd);
}
}
private void RemoveUnusedINotifyWatches(WatchedDirectory removedDir, int ignoredFd = -1)
{
Debug.Assert(!Monitor.IsEntered(removedDir.Watcher)); // We mustn't hold the watcher lock prior to taking the _addLock.
// _addLock stops handles from being added while we're removing watches.
// This is needed to prevent removing watch descriptors between INotifyAddWatch and adding them to the Watch.Watchers.
// _addLock is also used to synchronize with Stop.
_addLock.EnterWriteLock();
try
{
if (_isThreadStopping)
{
return;
}
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)
{
return;
}
lock (removedDir.Watcher)
{
if (removedDir.Children is { } children)
{
foreach (WatchedDirectory child in children)
{
RemoveINotifyWatchWhenNoMoreWatchers(child.Watch, ignoredFd);
}
}
}
}
finally
{
_addLock.ExitWriteLock();
}
void RemoveINotifyWatchWhenNoMoreWatchers(Watch watch, int ignoredFd)
{
lock (watch)
{
if (watch.Watchers.Count == 0)
{
if (_wdToWatch.TryRemove(watch.WatchDescriptor, out _))
{
if (watch.WatchDescriptor != ignoredFd)
{
Interop.Sys.INotifyRemoveWatch(_inotifyHandle, watch.WatchDescriptor);
}
}
}
}
}
}
private void RemoveWatchedDirectoryFromParentAndWatches(WatchedDirectory dir, ref bool removeINotifyWatches)
{
if (dir.IsRootDir)
{
lock (s_watchersLock)
{
_watchers.Remove(dir.Watcher);
// Set _allWatchersStopped before we update the Watch and _wdToWatch.
_allWatchersStopped = _watchers.Count == 0;
}
}
Watcher watcher = dir.Watcher;
lock (watcher)
{
if (dir.IsRootDir)
{
if (watcher.RootDirectory == null)
{
return; // Already removed.
}
watcher.RootDirectory = null;
}
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)
{
foreach (var child in children)
{
RemoveFromWatch(child, ref removeINotifyWatches);
}
}
}
static void RemoveFromWatch(WatchedDirectory dir, ref bool removeINotifyWatches)
{
Watch watch = dir.Watch;
lock (watch)
{
watch.Watchers.Remove(dir);
removeINotifyWatches |= watch.Watchers.Count == 0;
}
}
}
private void ProcessEvents()
{
try
{
lock (s_watchersLock)
{
// We've started an INotify, but no root watch was created for the FileSystemWatcher that started it.
if (_watchers.Count == 0)
{
StopINotify();
return;
}
}
// Carry over information from MOVED_FROM to MOVED_TO events.
int movedFromWatchCount = 0;
string movedFromName = "";
uint movedFromCookie = 0;
bool movedFromIsDir = false;
while (TryReadEvent(out NotifyEvent nextEvent))
{
if (!ProcessEvent(nextEvent, ref movedFromWatchCount, ref movedFromName, ref movedFromCookie, ref movedFromIsDir))
break;
}
}
catch (Exception ex)
{
lock (s_watchersLock)
{
StopINotify();
foreach (var watcher in _watchers)
{
watcher.QueueError(ex);
}
}
}
finally
{
Debug.Assert(_inotifyHandle.IsClosed);
}
}
private bool ProcessEvent(NotifyEvent nextEvent, ref int movedFromWatchCount, ref string movedFromName, ref uint movedFromCookie, ref bool movedFromIsDir)
{
// 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<char> 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;
}
// 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<WatchedDirectory> movedFromDirs = _dirBuffer.AsSpan(0, movedFromWatchCount);
// Look up the Watch in _wdToWatch.
// Synchronize with AddOrUpdateWatchedDirectory to 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<WatchedDirectory> 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)
{
bool isMatchingMovedTo = (mask & Interop.Sys.NotifyEvents.IN_MOVED_TO) != 0 && movedFromCookie == nextEvent.cookie;
foreach (var movedFrom in movedFromDirs)
{
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;
}
}
// 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)
{
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)
{
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;
foreach (WatchedDirectory dir in dirs)
{
Watcher watcher = dir.Watcher;
WatchedDirectory? matchingFromFound = null; // cache FindMatchingFrom result.
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 (FindMatchingFrom(movedFromDirs) is WatchedDirectory matchingFrom)
{
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;
}
// To match Windows, don't emit events for the root directory.
if (dir.IsRootDir && nextEvent.name.Length == 0)
{
continue;
}
if (IsIgnoredEvent(watcher, mask, isDir))
{
continue;
}
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 (FindMatchingFrom(movedFromDirs) is WatchedDirectory matchingFrom)
{
watcher.QueueEvent(WatcherEvent.Renamed(dir, nextEvent.name, matchingFrom, movedFromName));
}
else
{
watcher.QueueEvent(WatcherEvent.Created(dir, nextEvent.name));
}
break;
}
WatchedDirectory? FindMatchingFrom(ReadOnlySpan<WatchedDirectory> dirs)
=> matchingFromFound ??= (mask & Interop.Sys.NotifyEvents.IN_MOVED_TO) != 0 ? FindMatchingWatchedDirectory(dirs, watcher) : null;
}
// 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)
{
StopINotify();
return false;
}
return true;
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);
}
}
static ReadOnlySpan<WatchedDirectory> GetWatchedDirectories(Watch watch, ref WatchedDirectory[] buffer, int offset)
{
lock (watch)
{
int watchersCount = watch.Watchers.Count;
int lengthNeeded = watchersCount + offset;
if (lengthNeeded > buffer.Length)
{
Array.Resize(ref buffer, lengthNeeded);
}
watch.Watchers.CopyTo(buffer.AsSpan(offset));
return buffer.AsSpan(offset, watchersCount);
}
}
static WatchedDirectory? FindMatchingWatchedDirectory(ReadOnlySpan<WatchedDirectory> dir, Watcher watcher)
{
foreach (var d in dir)
{
if (d.Watcher == watcher)
{
return d;
}
}
return null;
}
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)));
}
}
private void RenameWatchedDirectories(WatchedDirectory moveTo, string moveToName, WatchedDirectory moveFrom, string moveFromName)
{
WatchedDirectory? sourceToRemove = null;
Watcher watcher = moveFrom.Watcher;
Debug.Assert(moveTo.Watcher == watcher);
lock (watcher)
{
int sourceIdx = moveFrom.FindChild(moveFromName);
if (sourceIdx == -1)
{
// unexpected: source not found.
return;
}
WatchedDirectory source = moveFrom.Children![sourceIdx];
int dstIdx = moveTo.FindChild(moveToName);
if (dstIdx != -1)
{
// unexpected: the destination already exists. Leave it and stop watching the source.
sourceToRemove = source;
}
else
{
// We'll re-use the Watches.
moveFrom.Children.RemoveAt(sourceIdx);
WatchedDirectory renamed = CreateWatchedDirectoryFrom(moveTo, source, moveToName);
moveTo.InitializedChildren.Add(renamed);
}
}
if (sourceToRemove is not null)
{
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;
}
}
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 >= 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 + 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 + INotifyEventSize, nameLength); // +16 to get past wd, mask, cookie, len
_bufferPos += 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;
}
internal struct WatcherEvent
{
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;
}
public static WatcherEvent Deleted(WatchedDirectory dir, string name)
=> new WatcherEvent(WatcherChangeTypes.Deleted, dir, name);
public static WatcherEvent Created(WatchedDirectory dir, string name)
=> new WatcherEvent(WatcherChangeTypes.Created, dir, name);
public static WatcherEvent Changed(WatchedDirectory dir, string name)
=> new WatcherEvent(WatcherChangeTypes.Changed, dir, name);
public static WatcherEvent Renamed(WatchedDirectory dir, string name, WatchedDirectory oldDir, string oldName)
=> new WatcherEvent(WatcherChangeTypes.Renamed, dir, name, oldDir, oldName);
public static WatcherEvent Error(Exception exception)
=> new WatcherEvent(exception);
public ReadOnlySpan<char> GetName(Span<char> pathBuffer)
=> Directory!.GetPath(Name, pathBuffer);
public ReadOnlySpan<char> GetOldName(Span<char> 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 };
/// <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> _weakFsw;
private readonly Channel<WatcherEvent> _eventQueue;
private INotify? _inotify;
private bool _emitEvents;
public string BasePath { get; }
public NotifyFilters NotifyFilters { get; }
public Interop.Sys.NotifyEvents WatchFilters { get; }
public bool IncludeSubdirectories { get; }
public bool IsWatcherStopped { get; set; }
public WatchedDirectory? RootDirectory
{
get
{
Debug.Assert(Monitor.IsEntered(this));
return field;
}
set
{
Debug.Assert(Monitor.IsEntered(this));
field = value;
}
}
public Watcher(FileSystemWatcher fsw)
{
_weakFsw = new WeakReference<FileSystemWatcher>(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.
// If InternalBufferSize were used, RestartForInternalBufferSize can be removed/should be updated.
_eventQueue = Channel.CreateUnbounded<WatcherEvent>(new UnboundedChannelOptions() { AllowSynchronousContinuations = false, SingleReader = true });
}
public void Start()
{
Debug.Assert(_inotify is null);
INotify? inotify;
WatchedDirectory? rootDirectory;
lock (s_watchersLock)
{
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);
}
}
_ = DequeueEvents();
if (rootDirectory is not null && IncludeSubdirectories)
{
WatchChildDirectories(rootDirectory, BasePath, includeBasePath: false);
}
_emitEvents = true;
WatchedDirectory? CreateRootWatch()
=> _inotify.AddOrUpdateWatchedDirectory(this, parent: null, BasePath, WatchFilters, ignoreMissing: false);
}
public void Stop()
{
WatchedDirectory? root;
lock (this)
{
if (IsWatcherStopped)
{
return;
}
IsWatcherStopped = true;
_emitEvents = false;
root = RootDirectory;
}
_eventQueue.Writer.Complete();
if (root is not null)
{
Debug.Assert(_inotify is not null);
_inotify.RemoveWatchedDirectory(root);
}
}
private async Task DequeueEvents()
{
char[] pathBuffer = new char[PATH_MAX];
try
{
await foreach (WatcherEvent @event in _eventQueue.Reader.ReadAllAsync().ConfigureAwait(false))
{
if (IsWatcherStopped)
{
break;
}
EmitEvent(@event, pathBuffer);
}
}
catch (Exception ex)
{
if (!IsWatcherStopped)
{
Stop();
try
{
Fsw?.OnError(new ErrorEventArgs(ex));
}
catch
{ }
}
}
}
private void EmitEvent(WatcherEvent @event, char[] pathBuffer)
{
FileSystemWatcher? fsw = Fsw;
if (fsw is null)
{
return;
}
switch (@event.Type)
{
case WatcherEvent.ErrorType:
fsw.OnError(new ErrorEventArgs(@event.Exception!));
// On InternalBufferOverflowException, the inotify is stopped.
// If the Watcher wasn't stopped, Restart it against a new inotify instance.
if (@event.Exception is InternalBufferOverflowException)
{
if (!IsWatcherStopped)
{
fsw.Restart();
}
}
break;
case WatcherChangeTypes.Created:
case WatcherChangeTypes.Deleted:
case WatcherChangeTypes.Changed:
{
ReadOnlySpan<char> name = @event.GetName(pathBuffer);
fsw.NotifyFileSystemEventArgs(@event.Type, name);
}
break;
case WatcherChangeTypes.Renamed:
{
string name = @event.GetName(pathBuffer).ToString();
ReadOnlySpan<char> oldName = @event.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 (IsWatcherStopped)
{
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 (string childDir in Directory.EnumerateDirectories(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, 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 (IsWatcherStopped)
{
return;
}
_eventQueue.Writer.TryWrite(WatcherEvent.Error(exception));
}
private FileSystemWatcher? Fsw
{
get
{
_weakFsw.TryGetTarget(out FileSystemWatcher? watcher);
return watcher;
}
}
/// <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;
// 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<WatchedDirectory>? _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<WatchedDirectory>? Children
{
get
{
Debug.Assert(Monitor.IsEntered(Watcher));
return _children;
}
set
{
Debug.Assert(Monitor.IsEntered(Watcher));
_children = value;
}
}
public List<WatchedDirectory> InitializedChildren => Children ??= new List<WatchedDirectory>();
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)
{
return i;
}
}
return -1;
}
internal ReadOnlySpan<char> GetPath(ReadOnlySpan<char> childName, Span<char> 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<char> pathBuffer, ReadOnlySpan<char> 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<WatchedDirectory> _watchers = new();
public int WatchDescriptor { get; }
public List<WatchedDirectory> Watchers
{
get
{
Debug.Assert(Monitor.IsEntered(this));
return _watchers;
}
}
public Watch(int watchDescriptor)
{
WatchDescriptor = watchDescriptor;
}
}
}
}
}
|