22// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
44using System ;
5+ using System . Diagnostics ;
56using System . IO ;
7+ using System . Threading ;
68using Microsoft . Extensions . FileProviders . Internal ;
79using Microsoft . Extensions . FileProviders . Physical ;
810using 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