diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFileProvider.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFileProvider.cs index ea06ffb247e8a5..9ac30856c56a8b 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFileProvider.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFileProvider.cs @@ -159,7 +159,10 @@ internal PhysicalFilesWatcher FileWatcher internal PhysicalFilesWatcher CreateFileWatcher() { string root = PathUtils.EnsureTrailingSlash(Path.GetFullPath(Root)); - return new PhysicalFilesWatcher(root, new FileSystemWatcher(root), UsePollingFileWatcher, _filters) + + // When both UsePollingFileWatcher & UseActivePolling are set, we won't use a FileSystemWatcher. + FileSystemWatcher watcher = UsePollingFileWatcher && UseActivePolling ? null : new FileSystemWatcher(root); + return new PhysicalFilesWatcher(root, watcher, UsePollingFileWatcher, _filters) { UseActivePolling = UseActivePolling, }; diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs index b819c83cdb7025..f9f122313535cc 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/PhysicalFilesWatcher.cs @@ -79,14 +79,23 @@ public PhysicalFilesWatcher( bool pollForChanges, ExclusionFilters filters) { + if (fileSystemWatcher == null && !pollForChanges) + { + throw new ArgumentNullException(nameof(fileSystemWatcher), SR.Error_FileSystemWatcherRequiredWithoutPolling); + } + _root = root; - _fileWatcher = fileSystemWatcher; - _fileWatcher.IncludeSubdirectories = true; - _fileWatcher.Created += OnChanged; - _fileWatcher.Changed += OnChanged; - _fileWatcher.Renamed += OnRenamed; - _fileWatcher.Deleted += OnChanged; - _fileWatcher.Error += OnError; + + if (fileSystemWatcher != null) + { + _fileWatcher = fileSystemWatcher; + _fileWatcher.IncludeSubdirectories = true; + _fileWatcher.Created += OnChanged; + _fileWatcher.Changed += OnChanged; + _fileWatcher.Renamed += OnRenamed; + _fileWatcher.Deleted += OnChanged; + _fileWatcher.Error += OnError; + } PollForChanges = pollForChanges; _filters = filters; @@ -361,27 +370,33 @@ private void ReportChangeForMatchedEntries(string path) private void TryDisableFileSystemWatcher() { - lock (_fileWatcherLock) + if (_fileWatcher != null) { - if (_filePathTokenLookup.IsEmpty && - _wildcardTokenLookup.IsEmpty && - _fileWatcher.EnableRaisingEvents) + lock (_fileWatcherLock) { - // Perf: Turn off the file monitoring if no files to monitor. - _fileWatcher.EnableRaisingEvents = false; + if (_filePathTokenLookup.IsEmpty && + _wildcardTokenLookup.IsEmpty && + _fileWatcher.EnableRaisingEvents) + { + // Perf: Turn off the file monitoring if no files to monitor. + _fileWatcher.EnableRaisingEvents = false; + } } } } private void TryEnableFileSystemWatcher() { - lock (_fileWatcherLock) + if (_fileWatcher != null) { - if ((!_filePathTokenLookup.IsEmpty || !_wildcardTokenLookup.IsEmpty) && - !_fileWatcher.EnableRaisingEvents) + lock (_fileWatcherLock) { - // Perf: Turn off the file monitoring if no files to monitor. - _fileWatcher.EnableRaisingEvents = true; + if ((!_filePathTokenLookup.IsEmpty || !_wildcardTokenLookup.IsEmpty) && + !_fileWatcher.EnableRaisingEvents) + { + // Perf: Turn off the file monitoring if no files to monitor. + _fileWatcher.EnableRaisingEvents = true; + } } } } diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Resources/Strings.resx new file mode 100644 index 00000000000000..061407cc73ac61 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/src/Resources/Strings.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The fileSystemWatcher parameter must be non-null when pollForChanges is false. + + \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs index 0d9fb9577845c2..97b633808f4533 100644 --- a/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs +++ b/src/libraries/Microsoft.Extensions.FileProviders.Physical/tests/PhysicalFileProviderTests.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.FileProviders.Internal; using Microsoft.Extensions.FileProviders.Physical; @@ -16,6 +18,7 @@ namespace Microsoft.Extensions.FileProviders public class PhysicalFileProviderTests { private const int WaitTimeForTokenToFire = 500; + private const int WaitTimeForTokenCallback = 10000; [Fact] public void GetFileInfoReturnsNotFoundFileInfoForNullPath() @@ -87,6 +90,54 @@ public void GetFileInfoReturnsNotFoundFileInfoForIllegalPathWithLeadingSlashes_U GetFileInfoReturnsNotFoundFileInfoForIllegalPathWithLeadingSlashes(path); } + [Fact] + [PlatformSpecific(TestPlatforms.Linux)] + public void PollingFileProviderShouldntConsumeINotifyInstances() + { + List disposables = new List(); + using (var root = new DisposableFileSystem()) + { + string maxInstancesFile = "/proc/sys/fs/inotify/max_user_instances"; + Assert.True(File.Exists(maxInstancesFile)); + int maxInstances = int.Parse(File.ReadAllText(maxInstancesFile)); + + // choose an arbitrary number that exceeds max + int instances = maxInstances + 16; + + AutoResetEvent are = new AutoResetEvent(false); + + var oldPollingInterval = PhysicalFilesWatcher.DefaultPollingInterval; + try + { + PhysicalFilesWatcher.DefaultPollingInterval = TimeSpan.FromMilliseconds(WaitTimeForTokenToFire); + for (int i = 0; i < instances; i++) + { + PhysicalFileProvider pfp = new PhysicalFileProvider(root.RootPath) + { + UsePollingFileWatcher = true, + UseActivePolling = true + }; + disposables.Add(pfp); + disposables.Add(pfp.Watch("*").RegisterChangeCallback(_ => are.Set(), null)); + } + + // trigger an event + root.CreateFile("test.txt"); + + // wait for at least one event. + Assert.True(are.WaitOne(WaitTimeForTokenCallback)); + } + finally + { + PhysicalFilesWatcher.DefaultPollingInterval = oldPollingInterval; + foreach (var disposable in disposables) + { + disposable.Dispose(); + } + } + } + } + private void GetFileInfoReturnsNotFoundFileInfoForIllegalPathWithLeadingSlashes(string path) { using (var provider = new PhysicalFileProvider(Path.GetTempPath())) @@ -1479,5 +1530,30 @@ public void CreateFileWatcher_CreatesWatcherWithPollingAndActiveFlags() Assert.True(fileWatcher.UseActivePolling); } } + + [Theory] + [InlineData(false)] + [InlineData(true)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/34580", TestPlatforms.Windows, TargetFrameworkMonikers.Netcoreapp, TestRuntimes.Mono)] + public async Task CanDeleteWatchedDirectory(bool useActivePolling) + { + using (var root = new DisposableFileSystem()) + using (var provider = new PhysicalFileProvider(root.RootPath)) + { + var fileName = Path.GetRandomFileName(); + PollingFileChangeToken.PollingInterval = TimeSpan.FromMilliseconds(10); + + provider.UsePollingFileWatcher = true; // We must use polling due to https://github.com/dotnet/runtime/issues/44484 + provider.UseActivePolling = useActivePolling; + + root.CreateFile(fileName); + var token = provider.Watch(fileName); + Directory.Delete(root.RootPath, true); + + await Task.Delay(WaitTimeForTokenToFire).ConfigureAwait(false); + + Assert.True(token.HasChanged); + } + } } }