Skip to content
This repository was archived by the owner on Nov 6, 2018. It is now read-only.

Commit b9986fc

Browse files
committed
Allow file watcher to actively poll for changes
The ChangeToken.OnChange pattern that's commonly used by providers to listen to changes requires IChangeToken to be active. The only two instances in the framework that does not support are the PollingChangeToken. This change makes the polling be active unless configured otherwise. Fixes aspnet/Mvc#8173
1 parent 4e744b9 commit b9986fc

9 files changed

+519
-75
lines changed
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Threading;
5+
using Microsoft.Extensions.Primitives;
6+
7+
namespace Microsoft.Extensions.FileProviders
8+
{
9+
internal interface IPollingChangeToken : IChangeToken
10+
{
11+
CancellationTokenSource CancellationTokenSource { get; }
12+
}
13+
}

src/FS.Physical/PhysicalFileProvider.cs

+122-20
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Diagnostics;
56
using System.IO;
7+
using System.Threading;
68
using Microsoft.Extensions.FileProviders.Internal;
79
using Microsoft.Extensions.FileProviders.Physical;
810
using Microsoft.Extensions.FileProviders.Physical.Internal;
@@ -20,19 +22,25 @@ namespace Microsoft.Extensions.FileProviders
2022
public class PhysicalFileProvider : IFileProvider, IDisposable
2123
{
2224
private const string PollingEnvironmentKey = "DOTNET_USE_POLLING_FILE_WATCHER";
23-
2425
private static readonly char[] _pathSeparators = new[]
2526
{Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar};
2627

27-
private readonly PhysicalFilesWatcher _filesWatcher;
2828
private readonly ExclusionFilters _filters;
2929

30+
private readonly Func<PhysicalFilesWatcher> _fileWatcherFactory;
31+
private PhysicalFilesWatcher _fileWatcher;
32+
private bool _fileWatcherInitialized;
33+
private object _fileWatcherLock = new object();
34+
35+
private bool? _usePollingFileWatcher;
36+
private bool? _useActivePolling;
37+
3038
/// <summary>
3139
/// Initializes a new instance of a PhysicalFileProvider at the given root directory.
3240
/// </summary>
3341
/// <param name="root">The root directory. This should be an absolute path.</param>
3442
public PhysicalFileProvider(string root)
35-
: this(root, CreateFileWatcher(root, ExclusionFilters.Sensitive), ExclusionFilters.Sensitive)
43+
: this(root, ExclusionFilters.Sensitive)
3644
{
3745
}
3846

@@ -42,20 +50,12 @@ public PhysicalFileProvider(string root)
4250
/// <param name="root">The root directory. This should be an absolute path.</param>
4351
/// <param name="filters">Specifies which files or directories are excluded.</param>
4452
public PhysicalFileProvider(string root, ExclusionFilters filters)
45-
: this(root, CreateFileWatcher(root, filters), filters)
46-
{ }
47-
48-
// for testing
49-
internal PhysicalFileProvider(string root, PhysicalFilesWatcher physicalFilesWatcher)
50-
: this(root, physicalFilesWatcher, ExclusionFilters.Sensitive)
51-
{ }
52-
53-
private PhysicalFileProvider(string root, PhysicalFilesWatcher physicalFilesWatcher, ExclusionFilters filters)
5453
{
5554
if (!Path.IsPathRooted(root))
5655
{
5756
throw new ArgumentException("The path must be absolute.", nameof(root));
5857
}
58+
5959
var fullRoot = Path.GetFullPath(root);
6060
// When we do matches in GetFullPath, we want to only match full directory names.
6161
Root = PathUtils.EnsureTrailingSlash(fullRoot);
@@ -64,28 +64,130 @@ private PhysicalFileProvider(string root, PhysicalFilesWatcher physicalFilesWatc
6464
throw new DirectoryNotFoundException(Root);
6565
}
6666

67-
_filesWatcher = physicalFilesWatcher;
6867
_filters = filters;
68+
_fileWatcherFactory = () => CreateFileWatcher();
69+
}
70+
71+
/// <summary>
72+
/// Gets or sets a value that determines if this instance of <see cref="PhysicalFileProvider"/>
73+
/// uses polling to determine file changes.
74+
/// <para>
75+
/// By default, <see cref="PhysicalFileProvider"/> uses <see cref="FileSystemWatcher"/> to listen to file change events
76+
/// for <see cref="Watch(string)"/>. <see cref="FileSystemWatcher"/> is ineffective in some scenarios such as mounted drives.
77+
/// Polling is required to effectively watch for file changes.
78+
/// </para>
79+
/// <seealso cref="UseActivePolling"/>.
80+
/// </summary>
81+
/// <value>
82+
/// The default value of this property is determined by the value of environment variable named <c>DOTNET_USE_POLLING_FILE_WATCHER</c>.
83+
/// When <c>true</c> or <c>1</c>, this property defaults to <c>true</c>; otherwise false.
84+
/// </value>
85+
public bool UsePollingFileWatcher
86+
{
87+
get
88+
{
89+
if (_fileWatcher != null)
90+
{
91+
throw new InvalidOperationException($"Cannot modify {nameof(UsePollingFileWatcher)} once file watcher has been initialized.");
92+
}
93+
94+
if (_usePollingFileWatcher == null)
95+
{
96+
ReadPollingEnvironmentVariables();
97+
}
98+
99+
return _usePollingFileWatcher.Value;
100+
}
101+
set => _usePollingFileWatcher = value;
102+
}
103+
104+
/// <summary>
105+
/// Gets or sets a value that determines if this instance of <see cref="PhysicalFileProvider"/>
106+
/// actively polls for file changes.
107+
/// <para>
108+
/// When <see langword="true"/>, <see cref="IChangeToken"/> returned by <see cref="Watch(string)"/> will actively poll for file changes
109+
/// (<see cref="IChangeToken.ActiveChangeCallbacks"/> will be <see langword="true"/>) instead of being passive.
110+
/// </para>
111+
/// <para>
112+
/// This property is only effective when <see cref="UsePollingFileWatcher"/> is set.
113+
/// </para>
114+
/// </summary>
115+
/// <value>
116+
/// The default value of this property is determined by the value of environment variable named <c>DOTNET_USE_POLLING_FILE_WATCHER</c>.
117+
/// When <c>true</c> or <c>1</c>, this property defaults to <c>true</c>; otherwise false.
118+
/// </value>
119+
public bool UseActivePolling
120+
{
121+
get
122+
{
123+
if (_useActivePolling == null)
124+
{
125+
ReadPollingEnvironmentVariables();
126+
}
127+
128+
return _useActivePolling.Value;
129+
}
130+
131+
set => _useActivePolling = value;
132+
}
133+
134+
internal PhysicalFilesWatcher FileWatcher
135+
{
136+
get
137+
{
138+
return LazyInitializer.EnsureInitialized(
139+
ref _fileWatcher,
140+
ref _fileWatcherInitialized,
141+
ref _fileWatcherLock,
142+
_fileWatcherFactory);
143+
}
144+
set
145+
{
146+
Debug.Assert(!_fileWatcherInitialized);
147+
148+
_fileWatcherInitialized = true;
149+
_fileWatcher = value;
150+
}
151+
}
152+
153+
internal PhysicalFilesWatcher CreateFileWatcher()
154+
{
155+
var root = PathUtils.EnsureTrailingSlash(Path.GetFullPath(Root));
156+
return new PhysicalFilesWatcher(root, new FileSystemWatcher(root), UsePollingFileWatcher, _filters)
157+
{
158+
UseActivePolling = UseActivePolling,
159+
};
69160
}
70161

71-
private static PhysicalFilesWatcher CreateFileWatcher(string root, ExclusionFilters filters)
162+
private void ReadPollingEnvironmentVariables()
72163
{
73164
var environmentValue = Environment.GetEnvironmentVariable(PollingEnvironmentKey);
74165
var pollForChanges = string.Equals(environmentValue, "1", StringComparison.Ordinal) ||
75-
string.Equals(environmentValue, "true", StringComparison.OrdinalIgnoreCase);
166+
string.Equals(environmentValue, "true", StringComparison.OrdinalIgnoreCase);
76167

77-
root = PathUtils.EnsureTrailingSlash(Path.GetFullPath(root));
78-
return new PhysicalFilesWatcher(root, new FileSystemWatcher(root), pollForChanges, filters);
168+
_usePollingFileWatcher = pollForChanges;
169+
_useActivePolling = pollForChanges;
79170
}
80171

81172
/// <summary>
82173
/// Disposes the provider. Change tokens may not trigger after the provider is disposed.
83174
/// </summary>
84-
public void Dispose()
175+
public void Dispose() => Dispose(true);
176+
177+
/// <summary>
178+
/// Disposes the provider.
179+
/// </summary>
180+
/// <param name="disposing"><c>true</c> is invoked from <see cref="IDisposable.Dispose"/>.</param>
181+
protected virtual void Dispose(bool disposing)
85182
{
86-
_filesWatcher.Dispose();
183+
_fileWatcher?.Dispose();
87184
}
88185

186+
/// <summary>
187+
/// Destructor for <see cref="PhysicalFileProvider"/>.
188+
/// </summary>
189+
~PhysicalFileProvider() => Dispose(false);
190+
89191
/// <summary>
90192
/// The root directory for this instance.
91193
/// </summary>
@@ -225,7 +327,7 @@ public IChangeToken Watch(string filter)
225327
// Relative paths starting with leading slashes are okay
226328
filter = filter.TrimStart(_pathSeparators);
227329

228-
return _filesWatcher.CreateFileChangeToken(filter);
330+
return FileWatcher.CreateFileChangeToken(filter);
229331
}
230332
}
231333
}

0 commit comments

Comments
 (0)