Allow watch of symbolic links to folders on Unix (#52679)
authorDavid CantĂș <dacantu@microsoft.com>
Wed, 26 May 2021 14:47:05 +0000 (09:47 -0500)
committerGitHub <noreply@github.com>
Wed, 26 May 2021 14:47:05 +0000 (07:47 -0700)
* Allow watch of symbolic links to folders on Unix

* Add logic to repeat directory tests with symlinks

* Remove whitespace

Co-authored-by: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com>
* Avoid follow symlinks by default in subdirectories

* Add Symbolic Link tests

* Revert "Add logic to repeat directory tests with symlinks"

This reverts commit 5b44eb0195c2e5824ac375862765ea5a2cdeb0ad.

* Exclude test from win7 and 8.1

* Avoid running Process.Start in iOS, tvOS and MacCatalyst

* Apply suggestions from code review

Co-authored-by: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com>
Co-authored-by: Carlos Sanchez <1175054+carlossanlop@users.noreply.github.com>
src/libraries/System.IO.FileSystem.Watcher/src/System/IO/FileSystemWatcher.Linux.cs
src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.SymbolicLink.cs [new file with mode: 0644]
src/libraries/System.IO.FileSystem.Watcher/tests/System.IO.FileSystem.Watcher.Tests.csproj
src/libraries/System.IO.FileSystem.Watcher/tests/Utility/FileSystemWatcherTest.cs

index ed924f3..bbf4f86 100644 (file)
@@ -337,21 +337,24 @@ namespace System.IO
             /// <param name="directoryName">The new directory path to monitor, relative to the root.</param>
             private void AddDirectoryWatchUnlocked(WatchedDirectory? parent, string directoryName)
             {
-                string fullPath = parent != null ? parent.GetPath(false, directoryName) : directoryName;
+                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
-                Interop.Sys.FileStatus status = default(Interop.Sys.FileStatus);
-                if ((Interop.Sys.LStat(fullPath, out status) == 0) &&
+                // 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
+                // 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)
-                int wd = Interop.Sys.INotifyAddWatch(_inotifyHandle, fullPath, (uint)(this._watchFilters | Interop.Sys.NotifyEvents.IN_DONT_FOLLOW | Interop.Sys.NotifyEvents.IN_EXCL_UNLINK));
+                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,
@@ -400,9 +403,9 @@ namespace System.IO
                         }
 
                         directoryEntry.Parent = parent;
-                        if (parent != null)
+                        if (hasParent)
                         {
-                            parent.InitializedChildren.Add (directoryEntry);
+                            parent!.InitializedChildren.Add(directoryEntry);
                         }
                     }
                     directoryEntry.Name = directoryName;
@@ -416,9 +419,9 @@ namespace System.IO
                         WatchDescriptor = wd,
                         Name = directoryName
                     };
-                    if (parent != null)
+                    if (hasParent)
                     {
-                        parent.InitializedChildren.Add (directoryEntry);
+                        parent!.InitializedChildren.Add(directoryEntry);
                     }
                     _wdToPathMap.Add(wd, directoryEntry);
                     isNewDirectory = true;
diff --git a/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.SymbolicLink.cs b/src/libraries/System.IO.FileSystem.Watcher/tests/FileSystemWatcher.SymbolicLink.cs
new file mode 100644 (file)
index 0000000..f28fe61
--- /dev/null
@@ -0,0 +1,129 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Xunit;
+
+namespace System.IO.Tests
+{
+    [ActiveIssue("https://github.com/dotnet/runtime/issues/34583", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)]
+    [ConditionalClass(typeof(SymbolicLink_Changed_Tests), nameof(CanCreateSymbolicLinks))]
+    public class SymbolicLink_Changed_Tests : FileSystemWatcherTest
+    {
+        private string CreateSymbolicLinkToTarget(string targetPath, bool isDirectory, string linkPath = null)
+        {
+            linkPath ??= GetTestFilePath();
+            Assert.True(CreateSymLink(targetPath, linkPath, isDirectory));
+
+            return linkPath;
+        }
+
+        [Fact]
+        public void FileSystemWatcher_FileSymbolicLink_TargetsFile_Fails()
+        {
+            // Arrange
+            using var tempFile = new TempFile(GetTestFilePath());
+            string linkPath = CreateSymbolicLinkToTarget(tempFile.Path, isDirectory: false);
+
+            // Act - Assert
+            Assert.Throws<ArgumentException>(() => new FileSystemWatcher(linkPath));
+        }
+
+        // Windows 7 and 8.1 doesn't throw in this case, see https://github.com/dotnet/runtime/issues/53010.
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsWindows10OrLater))]
+        [PlatformSpecific(TestPlatforms.Windows)]
+        public void FileSystemWatcher_DirectorySymbolicLink_TargetsFile_Fails()
+        {
+            // Arrange
+            using var tempFile = new TempFile(GetTestFilePath());
+            string linkPath = CreateSymbolicLinkToTarget(tempFile.Path, isDirectory: true);
+            using var watcher = new FileSystemWatcher(linkPath);
+
+            // Act - Assert
+            Assert.Throws<FileNotFoundException>(() => watcher.EnableRaisingEvents = true);
+        }
+
+        [Fact]
+        [PlatformSpecific(TestPlatforms.Windows)]
+        public void FileSystemWatcher_DirectorySymbolicLink_TargetsSelf_Fails()
+        {
+            // Arrange
+            string linkPath = GetTestFilePath();
+            CreateSymbolicLinkToTarget(targetPath: linkPath, isDirectory: true, linkPath: linkPath);
+            using var watcher = new FileSystemWatcher(linkPath);
+
+            // Act - Assert
+            Assert.Throws<FileNotFoundException>(() => watcher.EnableRaisingEvents = true);
+        }
+
+        [Fact]
+        public void FileSystemWatcher_SymbolicLink_TargetsDirectory_Create()
+        {
+            // Arrange
+            using var tempDir = new TempDirectory(GetTestFilePath());
+            string linkPath = CreateSymbolicLinkToTarget(tempDir.Path, isDirectory: true);
+
+            using var watcher = new FileSystemWatcher(linkPath);
+            watcher.NotifyFilter = NotifyFilters.DirectoryName;
+
+            string subDirName = GetTestFileName();
+            string subDirPath = Path.Combine(tempDir.Path, subDirName);
+
+            // Act - Assert
+            ExpectEvent(watcher, WatcherChangeTypes.Created,
+                action: () => Directory.CreateDirectory(subDirPath),
+                cleanup: () => Directory.Delete(subDirPath),
+                expectedPath: Path.Combine(linkPath, subDirName));
+        }
+
+        [Fact]
+        public void FileSystemWatcher_SymbolicLink_TargetsDirectory_Create_IncludeSubdirectories()
+        {
+            // Arrange
+            const string subDir = "subDir";
+            const string subDirLv2 = "subDirLv2";
+            using var tempDir = new TempDirectory(GetTestFilePath());
+            using var tempSubDir = new TempDirectory(Path.Combine(tempDir.Path, subDir));
+
+            string linkPath = CreateSymbolicLinkToTarget(tempDir.Path, isDirectory: true);
+            using var watcher = new FileSystemWatcher(linkPath);
+            watcher.NotifyFilter = NotifyFilters.DirectoryName;
+
+            string subDirLv2Path = Path.Combine(tempSubDir.Path, subDirLv2);
+
+            // Act - Assert
+            ExpectNoEvent(watcher, WatcherChangeTypes.Created,
+                action: () => Directory.CreateDirectory(subDirLv2Path),
+                cleanup: () => Directory.Delete(subDirLv2Path));
+
+            // Turn include subdirectories on.
+            watcher.IncludeSubdirectories = true;
+
+            ExpectEvent(watcher, WatcherChangeTypes.Created,
+                action: () => Directory.CreateDirectory(subDirLv2Path),
+                cleanup: () => Directory.Delete(subDirLv2Path),
+                expectedPath: Path.Combine(linkPath, subDir, subDirLv2));
+        }
+
+        [Fact]
+        public void FileSystemWatcher_SymbolicLink_IncludeSubdirectories_DoNotDereferenceChildLink()
+        {
+            // Arrange
+            using var dirA = new TempDirectory(GetTestFilePath());
+            using var dirB = new TempDirectory(GetTestFilePath());
+
+            string linkPath = Path.Combine(dirA.Path, "linkToDirB");
+            CreateSymbolicLinkToTarget(dirB.Path, isDirectory: true, linkPath);
+
+            using var watcher = new FileSystemWatcher(dirA.Path);
+            watcher.NotifyFilter = NotifyFilters.DirectoryName;
+            watcher.IncludeSubdirectories = true;
+
+            string subDirPath = Path.Combine(linkPath, GetTestFileName());
+
+            // Act - Assert
+            ExpectNoEvent(watcher, WatcherChangeTypes.Created,
+                action: () => Directory.CreateDirectory(subDirPath),
+                cleanup: () => Directory.Delete(subDirPath));
+        }
+    }
+}
index 4c7b2ed..67c502f 100644 (file)
@@ -23,6 +23,7 @@
     <Compile Include="FileSystemWatcher.File.NotifyFilter.cs" />
     <Compile Include="FileSystemWatcher.InternalBufferSize.cs" />
     <Compile Include="FileSystemWatcher.MultipleWatchers.cs" />
+    <Compile Include="FileSystemWatcher.SymbolicLink.cs" />
     <Compile Include="FileSystemWatcher.WaitForChanged.cs" />
     <Compile Include="FileSystemWatcher.unit.cs" />
     <!-- Helpers -->
index 0d6ea8b..bdfaf50 100644 (file)
@@ -461,6 +461,11 @@ namespace System.IO.Tests
 
         public static bool CreateSymLink(string targetPath, string linkPath, bool isDirectory)
         {
+            if (OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsMacCatalyst()) // OSes that don't support Process.Start()
+            {
+                return false;
+            }
+            
             Process symLinkProcess = new Process();
             if (OperatingSystem.IsWindows())
             {