Handle FileNotFound for symlinks when using polling (#56915)
authorDavid CantĂș <dacantu@microsoft.com>
Tue, 10 Aug 2021 14:37:24 +0000 (07:37 -0700)
committerGitHub <noreply@github.com>
Tue, 10 Aug 2021 14:37:24 +0000 (16:37 +0200)
* Handle FileNotFound for symlinks when using polling

* Re-enable tests

* Use CTS instead of Task.Wait(TimeSpan) to fix issues in browser.

src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Internal/FileSystemInfoHelper.cs
src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs
src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.netcoreapp.cs

index 5ae854ae6efce8eed388de576a6cc2a91ca0bb29..ddd21332c732575b1cd98c1c155f41d22bfeb50b 100644 (file)
@@ -45,16 +45,28 @@ namespace Microsoft.Extensions.FileProviders.Physical
         // If file is a link, and link target does not exists, return DateTime.MinValue
         //   since the link's LastWriteTimeUtc doesn't convey anything for this scenario.
         // If file is not a link, return null to inform the caller that file is not a link.
-        public static DateTime? GetFileLinkTargetLastWriteTimeUtc(FileInfo fileInfo)
+        public static DateTime? GetFileLinkTargetLastWriteTimeUtc(FileInfo fileInfo, bool isSecondTry = false)
         {
 #if NETCOREAPP
             Debug.Assert(fileInfo.Exists);
             if (fileInfo.LinkTarget != null)
             {
-                FileSystemInfo targetInfo = fileInfo.ResolveLinkTarget(returnFinalTarget: true);
-                if (targetInfo.Exists)
+                try
                 {
-                    return targetInfo.LastWriteTimeUtc;
+                    FileSystemInfo targetInfo = fileInfo.ResolveLinkTarget(returnFinalTarget: true);
+                    if (targetInfo.Exists)
+                    {
+                        return targetInfo.LastWriteTimeUtc;
+                    }
+                }
+                catch (FileNotFoundException)
+                {
+                    // The file ceased to exist between LinkTarget and ResolveLinkTarget.
+                    // Try one more time, if it fails again just give up.
+                    if (!isSecondTry)
+                    {
+                        GetFileLinkTargetLastWriteTimeUtc(fileInfo, isSecondTry: true);
+                    }
                 }
 
                 return DateTime.MinValue;
index 80e94fc31f910c0d9e6ff18d3bb5711756048fb5..e1480ab8686b4dc24ba1ff4360c564c41013d948 100644 (file)
@@ -1540,7 +1540,6 @@ namespace Microsoft.Extensions.FileProviders
         [Theory]
         [InlineData(false)]
         [InlineData(true)]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/56190", TestPlatforms.AnyUnix)]
         public async Task UsePollingFileWatcher_UseActivePolling_HasChanged(bool useWildcard)
         {
             // Arrange
@@ -1550,25 +1549,27 @@ namespace Microsoft.Extensions.FileProviders
             File.WriteAllText(filePath, "v1.1");
 
             using var provider = new PhysicalFileProvider(root.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true };
-            IChangeToken token = provider.Watch(useWildcard ? "*" : fileName);
+            IChangeToken changeToken = provider.Watch(useWildcard ? "*" : fileName);
 
-            var tcs = new TaskCompletionSource<object>();
-            token.RegisterChangeCallback(_ => { tcs.TrySetResult(null); }, null);
+            var tcs = new TaskCompletionSource<bool>();
+            changeToken.RegisterChangeCallback(_ => { tcs.TrySetResult(true); }, null);
+
+            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+            cts.Token.Register(() => tcs.TrySetCanceled());
 
             // Act
             await Task.Delay(1000); // Wait a second before writing again, see https://github.com/dotnet/runtime/issues/55951.
             File.WriteAllText(filePath, "v1.2");
 
             // Assert
-            Assert.True(tcs.Task.Wait(TimeSpan.FromSeconds(30)),
+            Assert.True(await tcs.Task,
                 $"Change event was not raised - current time: {DateTime.UtcNow:O}, file LastWriteTimeUtc: {File.GetLastWriteTimeUtc(filePath):O}");
         }
 
         [Theory]
         [InlineData(false)]
         [InlineData(true)]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/56190", TestPlatforms.AnyUnix)]
-        public void UsePollingFileWatcher_UseActivePolling_HasChanged_FileDeleted(bool useWildcard)
+        public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_FileDeleted(bool useWildcard)
         {
             // Arrange
             using var root = new DisposableFileSystem();
@@ -1578,16 +1579,19 @@ namespace Microsoft.Extensions.FileProviders
 
             string filter = useWildcard ? "*" : fileName;
             using var provider = new PhysicalFileProvider(root.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true };
-            IChangeToken token = provider.Watch(filter);
+            IChangeToken changeToken = provider.Watch(filter);
+
+            var tcs = new TaskCompletionSource<bool>();
+            changeToken.RegisterChangeCallback(_ => { tcs.TrySetResult(true); }, null);
 
-            var tcs = new TaskCompletionSource<object>();
-            token.RegisterChangeCallback(_ => { tcs.TrySetResult(null); }, null);
+            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+            cts.Token.Register(() => tcs.TrySetCanceled());
 
             // Act
             File.Delete(filePath);
 
             // Assert
-            Assert.True(tcs.Task.Wait(TimeSpan.FromSeconds(30)),
+            Assert.True(await tcs.Task,
                 $"Change event was not raised - current time: {DateTime.UtcNow:O}, file Exists: {File.Exists(filePath)}.");
         }
 
index 7ccfafb28180cd150de2e42942bad24a95d05df3..c25b3b5e0e3328f416fe6e866aa27a3d7a1dc81d 100644 (file)
@@ -3,6 +3,7 @@
 
 using System;
 using System.IO;
+using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.Extensions.Primitives;
 using Xunit;
@@ -14,7 +15,6 @@ namespace Microsoft.Extensions.FileProviders
         [Theory]
         [InlineData(false)]
         [InlineData(true)]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/56190", TestPlatforms.AnyUnix)]
         public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink(bool useWildcard)
         {
             // Arrange
@@ -30,23 +30,25 @@ namespace Microsoft.Extensions.FileProviders
             using var provider = new PhysicalFileProvider(rootOfLink.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true };
             IChangeToken token = provider.Watch(useWildcard ? "*" : linkName);
 
-            var tcs = new TaskCompletionSource();
-            token.RegisterChangeCallback(_ => { tcs.TrySetResult(); }, null);
+            var tcs = new TaskCompletionSource<bool>();
+            token.RegisterChangeCallback(_ => { tcs.TrySetResult(true); }, null);
+
+            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+            cts.Token.Register(() => tcs.TrySetCanceled());
 
             // Act
             await Task.Delay(1000); // Wait a second before writing again, see https://github.com/dotnet/runtime/issues/55951.
             File.WriteAllText(filePath, "v1.2");
 
             // Assert
-            Assert.True(tcs.Task.Wait(TimeSpan.FromSeconds(30)),
+            Assert.True(await tcs.Task,
                 $"Change event was not raised - current time: {DateTime.UtcNow:O}, file LastWriteTimeUtc: {File.GetLastWriteTimeUtc(filePath):O}.");
         }
 
         [Theory]
         [InlineData(false)]
         [InlineData(true)]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/56190", TestPlatforms.AnyUnix)]
-        public void UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink_TargetNotExists(bool useWildcard)
+        public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink_TargetNotExists(bool useWildcard)
         {
             // Arrange
             using var rootOfLink = new DisposableFileSystem();
@@ -59,11 +61,12 @@ namespace Microsoft.Extensions.FileProviders
             IChangeToken token = provider.Watch(useWildcard ? "*" : linkName);
 
             var tcs = new TaskCompletionSource();
-            token.RegisterChangeCallback(_ => { tcs.TrySetResult(); }, null);
+            token.RegisterChangeCallback(_ => { Assert.True(false, "Change event was raised when it was not expected."); }, null);
 
-            // Assert
-            Assert.False(tcs.Task.Wait(TimeSpan.FromSeconds(30)),
-                "Change event was raised when it was not expected.");
+            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+            cts.Token.Register(() => tcs.TrySetCanceled());
+
+            await Assert.ThrowsAsync<TaskCanceledException>(() => tcs.Task);
         }
 
         [Theory]
@@ -71,7 +74,6 @@ namespace Microsoft.Extensions.FileProviders
         [InlineData(false, true)]
         [InlineData(true, false)]
         [InlineData(true, true)]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/56190", TestPlatforms.AnyUnix)]
         public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink_TargetChanged(bool useWildcard, bool linkWasBroken)
         {
             // Arrange
@@ -96,23 +98,25 @@ namespace Microsoft.Extensions.FileProviders
             using var provider = new PhysicalFileProvider(rootOfLink.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true };
             IChangeToken token = provider.Watch(filter);
 
-            var tcs = new TaskCompletionSource();
-            token.RegisterChangeCallback(_ => { tcs.TrySetResult(); }, null);
+            var tcs = new TaskCompletionSource<bool>();
+            token.RegisterChangeCallback(_ => { tcs.TrySetResult(true); }, null);
+
+            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+            cts.Token.Register(() => tcs.TrySetCanceled());
 
             // Act - Change link target to file 2.
             File.Delete(linkPath);
             File.CreateSymbolicLink(linkPath, file2Path);
 
             // Assert - It should report the change regardless of the timestamp being older.
-            Assert.True(tcs.Task.Wait(TimeSpan.FromSeconds(30)),
+            Assert.True(await tcs.Task,
                 $"Change event was not raised - current time: {DateTime.UtcNow:O}, file1 LastWriteTimeUtc: {File.GetLastWriteTimeUtc(file1Path):O}, file2 LastWriteTime: {File.GetLastWriteTimeUtc(file2Path):O}.");
         }
 
         [Theory]
         [InlineData(false)]
         [InlineData(true)]
-        [ActiveIssue("https://github.com/dotnet/runtime/issues/56190", TestPlatforms.AnyUnix)]
-        public void UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink_TargetDeleted(bool useWildcard)
+        public async Task UsePollingFileWatcher_UseActivePolling_HasChanged_SymbolicLink_TargetDeleted(bool useWildcard)
         {
             // Arrange
             using var rootOfFile = new DisposableFileSystem();
@@ -129,14 +133,17 @@ namespace Microsoft.Extensions.FileProviders
             using var provider = new PhysicalFileProvider(rootOfLink.RootPath) { UsePollingFileWatcher = true, UseActivePolling = true };
             IChangeToken token = provider.Watch(filter);
 
-            var tcs = new TaskCompletionSource();
-            token.RegisterChangeCallback(_ => { tcs.TrySetResult(); }, null);
+            var tcs = new TaskCompletionSource<bool>();
+            token.RegisterChangeCallback(_ => { tcs.TrySetResult(true); }, null);
+
+            var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
+            cts.Token.Register(() => tcs.TrySetCanceled());
 
             // Act
             File.Delete(linkPath);
 
             // Assert
-            Assert.True(tcs.Task.Wait(TimeSpan.FromSeconds(30)),
+            Assert.True(await tcs.Task,
                 $"Change event was not raised - current time: {DateTime.UtcNow:O}, file LastWriteTimeUtc: {File.GetLastWriteTimeUtc(filePath):O}.");
         }
     }