Skip to content

Commit c52b3a7

Browse files
committed
FileSystemWatcher.Linux/OSX: raise Error event when FileSystemWatcher's watched directory is deleted or moved.
1 parent 5cc6b8b commit c52b3a7

8 files changed

Lines changed: 80 additions & 48 deletions

File tree

src/libraries/Common/src/Interop/Linux/System.Native/Interop.INotify.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ internal enum NotifyEvents
5151
IN_MOVED_TO = 0x00000080,
5252
IN_CREATE = 0x00000100,
5353
IN_DELETE = 0x00000200,
54+
IN_MOVE_SELF = 0x00000800,
5455
IN_Q_OVERFLOW = 0x00004000,
5556
IN_IGNORED = 0x00008000,
5657
IN_ONLYDIR = 0x01000000,

src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs

Lines changed: 26 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -525,56 +525,42 @@ private void TryEnableFileSystemWatcher()
525525
return;
526526
}
527527

528-
if (!_filePathTokenLookup.IsEmpty || !_wildcardTokenLookup.IsEmpty)
528+
if (!_fileWatcher.EnableRaisingEvents && (!_filePathTokenLookup.IsEmpty || !_wildcardTokenLookup.IsEmpty))
529529
{
530-
bool rootExists = Directory.Exists(_root);
531-
532-
// On some platforms (e.g., Linux), FileSystemWatcher currently does not
533-
// invoke OnError when the watched directory is deleted, so we don't disable
534-
// the FSW and start root watcher at that point.
535-
// Detect and handle this opportunistically now.
536-
if (_fileWatcher.EnableRaisingEvents && !rootExists)
530+
if (!Directory.Exists(_root))
537531
{
538-
_fileWatcher.EnableRaisingEvents = false;
532+
needsRootWatcher = true;
533+
_rootWasUnavailable = true;
539534
}
540-
541-
if (!_fileWatcher.EnableRaisingEvents)
535+
else
542536
{
543-
if (!rootExists)
537+
try
544538
{
545-
needsRootWatcher = true;
546-
_rootWasUnavailable = true;
539+
if (string.IsNullOrEmpty(_fileWatcher.Path))
540+
{
541+
_fileWatcher.Path = _root;
542+
}
543+
544+
_fileWatcher.EnableRaisingEvents = true;
545+
546+
// Only scan for existing entries if the FSW was enabled after _root
547+
// was initially missing (i.e. we went through the PCW path). In the
548+
// normal case where _root always existed, there is no gap to cover.
549+
justEnabledAfterRootCreated = _rootWasUnavailable;
550+
_rootWasUnavailable = false;
547551
}
548-
else
552+
catch (Exception ex) when (ex is ArgumentException or IOException)
549553
{
550-
try
554+
// _root may have been deleted between the Directory.Exists check
555+
// and the property sets above. Fall back to watching for root creation.
556+
if (!Directory.Exists(_root))
551557
{
552-
if (string.IsNullOrEmpty(_fileWatcher.Path))
553-
{
554-
_fileWatcher.Path = _root;
555-
}
556-
557-
_fileWatcher.EnableRaisingEvents = true;
558-
559-
// Only scan for existing entries if the FSW was enabled after _root
560-
// was initially missing (i.e. we went through the PCW path). In the
561-
// normal case where _root always existed, there is no gap to cover.
562-
justEnabledAfterRootCreated = _rootWasUnavailable;
563-
_rootWasUnavailable = false;
558+
needsRootWatcher = true;
559+
_rootWasUnavailable = true;
564560
}
565-
catch (Exception ex) when (ex is ArgumentException or IOException)
561+
else
566562
{
567-
// _root may have been deleted between the Directory.Exists check
568-
// and the property sets above. Fall back to watching for root creation.
569-
if (!Directory.Exists(_root))
570-
{
571-
needsRootWatcher = true;
572-
_rootWasUnavailable = true;
573-
}
574-
else
575-
{
576-
throw;
577-
}
563+
throw;
578564
}
579565
}
580566
}

src/libraries/System.IO.FileSystem.Watcher/src/Resources/Strings.resx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,9 @@
125125
<data name="FSW_BufferOverflow" xml:space="preserve">
126126
<value>Too many changes at once in directory:{0}.</value>
127127
</data>
128+
<data name="FSW_WatchedDirectoryDeletedOrMoved" xml:space="preserve">
129+
<value>The directory being watched '{0}' was deleted or moved.</value>
130+
</data>
128131
<data name="InvalidDirName" xml:space="preserve">
129132
<value>The directory name {0} is invalid.</value>
130133
</data>

src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,8 @@ private void StopINotify()
242242
Interop.Sys.NotifyEvents mask = watchFilters |
243243
Interop.Sys.NotifyEvents.IN_ONLYDIR | // we only allow watches on directories
244244
Interop.Sys.NotifyEvents.IN_EXCL_UNLINK | // we want to stop monitoring unlinked files
245-
(parent == null ? 0 : Interop.Sys.NotifyEvents.IN_DONT_FOLLOW); // Follow links only for the root path, not the subdirs.
245+
(parent == null ? Interop.Sys.NotifyEvents.IN_MOVE_SELF // Detect when the root directory is moved.
246+
: Interop.Sys.NotifyEvents.IN_DONT_FOLLOW); // Follow links only for the root path, not the subdirs.
246247

247248
// To support multiple FileSystemWatchers on the same inotify instance, we need to use IN_MASK_ADD
248249
// so we don't remove events another watcher is interested in.
@@ -657,9 +658,17 @@ private unsafe bool ProcessEvent(NotifyEvent nextEvent, ref int movedFromWatchCo
657658
}
658659
}
659660
// IN_IGNORED: Watch was removed explicitly or automatically because the directory was deleted.
660-
if ((mask & Interop.Sys.NotifyEvents.IN_IGNORED) != 0)
661+
// IN_MOVE_SELF: A root directory was moved.
662+
if (((mask & Interop.Sys.NotifyEvents.IN_IGNORED) != 0)
663+
|| (((mask & Interop.Sys.NotifyEvents.IN_MOVE_SELF) != 0) && dir.IsRootDir))
661664
{
662-
RemoveWatchedDirectory(dir, ignoredFd: nextEvent.wd);
665+
int ignoredFd = (mask & Interop.Sys.NotifyEvents.IN_IGNORED) != 0 ? nextEvent.wd : -1;
666+
RemoveWatchedDirectory(dir, ignoredFd);
667+
668+
if (dir.IsRootDir && !watcher.IsWatcherStopped)
669+
{
670+
watcher.QueueError(CreateWatchedDirectoryDeletedOrMovedException(watcher.BasePath));
671+
}
663672
continue;
664673
}
665674

@@ -1248,10 +1257,7 @@ private static Interop.Sys.NotifyEvents TranslateFilters(NotifyFilters filters)
12481257

12491258
// For the Created and Deleted events, we need to always
12501259
// register for the created/deleted inotify events, regardless
1251-
// of the supplied filters values. We explicitly don't include IN_DELETE_SELF.
1252-
// The Windows implementation doesn't include notifications for the root directory,
1253-
// and having this for subdirectories results in duplicate notifications, one from
1254-
// the parent and one from self.
1260+
// of the supplied filters values.
12551261
result |=
12561262
Interop.Sys.NotifyEvents.IN_CREATE |
12571263
Interop.Sys.NotifyEvents.IN_DELETE;

src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.OSX.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,14 @@ private unsafe void ProcessEvents(int numEvents,
438438
ReadOnlySpan<char> path = parsedEvent.Path;
439439
Debug.Assert(path[^1] != '/', "Trailing slashes on events is not supported");
440440

441+
// Root was deleted/renamed.
442+
if (eventFlags[i].HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagRootChanged))
443+
{
444+
watcher.OnError(new ErrorEventArgs(CreateWatchedDirectoryDeletedOrMovedException(_fullDirectory)));
445+
CleanupEventStream();
446+
return;
447+
}
448+
441449
// Match Windows and don't notify us about changes to the Root folder
442450
if (_fullDirectory.Length >= path.Length && path.Equals(_fullDirectory.AsSpan(0, path.Length), StringComparison.OrdinalIgnoreCase))
443451
{
@@ -609,7 +617,6 @@ private static bool ShouldRescanOccur(FSEventStreamEventFlags flags)
609617
return (flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagMustScanSubDirs) ||
610618
flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagUserDropped) ||
611619
flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagKernelDropped) ||
612-
flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagRootChanged) ||
613620
flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagMount) ||
614621
flags.HasFlag(FSEventStreamEventFlags.kFSEventStreamEventFlagUnmount));
615622
}

src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,9 @@ private void NotifyInternalBufferOverflowEvent()
404404
private static InternalBufferOverflowException CreateBufferOverflowException(string directoryPath)
405405
=> new InternalBufferOverflowException(SR.Format(SR.FSW_BufferOverflow, directoryPath));
406406

407+
private static DirectoryNotFoundException CreateWatchedDirectoryDeletedOrMovedException(string directoryPath)
408+
=> new DirectoryNotFoundException(SR.Format(SR.FSW_WatchedDirectoryDeletedOrMoved, directoryPath));
409+
407410
/// <summary>
408411
/// Raises the event to each handler in the list.
409412
/// </summary>

src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Delete.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,5 +99,17 @@ public void FileSystemWatcher_Directory_Delete_SynchronizingObject()
9999
Assert.True(invoker.BeginInvoke_Called);
100100
}
101101
}
102+
103+
[Fact]
104+
public void FileSystemWatcher_WatchedDirectory_Delete()
105+
{
106+
string dir = CreateTestDirectory(TestDirectory, "watched");
107+
using var watcher = new FileSystemWatcher(dir);
108+
109+
Action action = () => Directory.Delete(dir, recursive: true);
110+
Action cleanup = () => Directory.CreateDirectory(dir);
111+
112+
ExpectError(watcher, action, cleanup);
113+
}
102114
}
103115
}

src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.Directory.Move.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,20 @@ public void Directory_Move_SynchronizingObject()
116116
}
117117
}
118118

119+
[Fact]
120+
[PlatformSpecific(TestPlatforms.AnyUnix)] // Windows directory handles follow the directory on move; no error is raised.
121+
public void FileSystemWatcher_WatchedDirectory_Move()
122+
{
123+
string dir = CreateTestDirectory(TestDirectory, "watched");
124+
string targetDir = Path.Combine(TestDirectory, "moved");
125+
using var watcher = new FileSystemWatcher(dir);
126+
127+
Action action = () => Directory.Move(dir, targetDir);
128+
Action cleanup = () => Directory.Move(targetDir, dir);
129+
130+
ExpectError(watcher, action, cleanup);
131+
}
132+
119133
#region Test Helpers
120134

121135
private void DirectoryMove_SameDirectory(WatcherChangeTypes eventType)

0 commit comments

Comments
 (0)