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

Commit c225155

Browse files
committed
Add support for polling file watcher
Fixes #181
1 parent f28b8c6 commit c225155

File tree

8 files changed

+347
-226
lines changed

8 files changed

+347
-226
lines changed

src/Microsoft.Extensions.FileProviders.Physical/FileChangeToken.cs

Lines changed: 0 additions & 69 deletions
This file was deleted.

src/Microsoft.Extensions.FileProviders.Physical/PhysicalFileProvider.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace Microsoft.Extensions.FileProviders
1515
/// </summary>
1616
public class PhysicalFileProvider : IFileProvider, IDisposable
1717
{
18+
private const string PollingEnvironmentKey = "ASPNETCORE_POLL_FOR_FILE_CHANGES";
1819
private static readonly char[] _invalidFileNameChars = Path.GetInvalidFileNameChars()
1920
.Where(c => c != Path.DirectorySeparatorChar && c != Path.AltDirectorySeparatorChar).ToArray();
2021
private static readonly char[] _pathSeparators = new[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar };
@@ -25,7 +26,7 @@ public class PhysicalFileProvider : IFileProvider, IDisposable
2526
/// </summary>
2627
/// <param name="root">The root directory. This should be an absolute path.</param>
2728
public PhysicalFileProvider(string root)
28-
: this(root, new PhysicalFilesWatcher(EnsureTrailingSlash(Path.GetFullPath(root))))
29+
: this(root, CreateFileWatcher(root))
2930
{
3031
}
3132

@@ -46,6 +47,16 @@ internal PhysicalFileProvider(string root, PhysicalFilesWatcher physicalFilesWat
4647
_filesWatcher = physicalFilesWatcher;
4748
}
4849

50+
private static PhysicalFilesWatcher CreateFileWatcher(string root)
51+
{
52+
var environmentValue = Environment.GetEnvironmentVariable(PollingEnvironmentKey);
53+
var pollForChanges = string.Equals(environmentValue, "1", StringComparison.Ordinal) ||
54+
string.Equals(environmentValue, "true", StringComparison.OrdinalIgnoreCase);
55+
56+
root = EnsureTrailingSlash(Path.GetFullPath(root));
57+
return new PhysicalFilesWatcher(root, new FileSystemWatcher(root), pollForChanges);
58+
}
59+
4960
public void Dispose()
5061
{
5162
_filesWatcher.Dispose();

src/Microsoft.Extensions.FileProviders.Physical/PhysicalFilesWatcher.cs

Lines changed: 123 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,29 @@
33

44
using System;
55
using System.Collections.Concurrent;
6+
using System.Collections.Generic;
67
using System.IO;
7-
using System.Linq;
8-
using System.Text.RegularExpressions;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.Extensions.FileSystemGlobbing;
11+
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
912
using Microsoft.Extensions.Primitives;
1013

1114
namespace Microsoft.Extensions.FileProviders.Physical
1215
{
1316
public class PhysicalFilesWatcher : IDisposable
1417
{
15-
private readonly ConcurrentDictionary<string, FileChangeToken> _tokenCache =
16-
new ConcurrentDictionary<string, FileChangeToken>(StringComparer.OrdinalIgnoreCase);
18+
private readonly ConcurrentDictionary<string, ChangeTokenInfo> _matchInfoCache =
19+
new ConcurrentDictionary<string, ChangeTokenInfo>(StringComparer.OrdinalIgnoreCase);
1720
private readonly FileSystemWatcher _fileWatcher;
1821
private readonly object _lockObject = new object();
1922
private readonly string _root;
23+
private readonly bool _pollForChanges;
2024

21-
public PhysicalFilesWatcher(string root)
22-
: this(root, new FileSystemWatcher(root))
23-
{
24-
}
25-
26-
public PhysicalFilesWatcher(string root, FileSystemWatcher fileSystemWatcher)
25+
public PhysicalFilesWatcher(
26+
string root,
27+
FileSystemWatcher fileSystemWatcher,
28+
bool pollForChanges)
2729
{
2830
_root = root;
2931
_fileWatcher = fileSystemWatcher;
@@ -33,30 +35,87 @@ public PhysicalFilesWatcher(string root, FileSystemWatcher fileSystemWatcher)
3335
_fileWatcher.Renamed += OnRenamed;
3436
_fileWatcher.Deleted += OnChanged;
3537
_fileWatcher.Error += OnError;
38+
39+
_pollForChanges = pollForChanges;
3640
}
3741

38-
internal IChangeToken CreateFileChangeToken(string filter)
42+
public IChangeToken CreateFileChangeToken(string filter)
3943
{
40-
filter = NormalizeFilter(filter);
41-
var pattern = WildcardToRegexPattern(filter);
44+
if (filter == null)
45+
{
46+
throw new ArgumentNullException(nameof(filter));
47+
}
48+
49+
filter = NormalizePath(filter);
50+
51+
IChangeToken changeToken;
52+
var isWildCard = filter.IndexOf('*') != -1;
53+
if (isWildCard || IsDirectoryPath(filter))
54+
{
55+
changeToken = ResolveFileTokensForGlobbingPattern(filter);
56+
}
57+
else
58+
{
59+
changeToken = GetOrAddChangeToken(filter);
60+
}
4261

43-
FileChangeToken changeToken;
44-
if (!_tokenCache.TryGetValue(pattern, out changeToken))
62+
lock (_lockObject)
4563
{
46-
changeToken = _tokenCache.GetOrAdd(pattern, new FileChangeToken(pattern));
47-
lock (_lockObject)
64+
if (_matchInfoCache.Count > 0 && !_fileWatcher.EnableRaisingEvents)
4865
{
49-
if (_tokenCache.Count > 0 && !_fileWatcher.EnableRaisingEvents)
50-
{
51-
// Perf: Turn on the file monitoring if there is something to monitor.
52-
_fileWatcher.EnableRaisingEvents = true;
53-
}
66+
// Perf: Turn on the file monitoring if there is something to monitor.
67+
_fileWatcher.EnableRaisingEvents = true;
5468
}
5569
}
5670

5771
return changeToken;
5872
}
5973

74+
private IChangeToken GetOrAddChangeToken(string filePath)
75+
{
76+
ChangeTokenInfo tokenInfo;
77+
if (!_matchInfoCache.TryGetValue(filePath, out tokenInfo))
78+
{
79+
var cancellationTokenSource = new CancellationTokenSource();
80+
var cancellationChangeToken = new CancellationChangeToken(cancellationTokenSource.Token);
81+
tokenInfo = new ChangeTokenInfo(cancellationTokenSource, cancellationChangeToken);
82+
tokenInfo = _matchInfoCache.GetOrAdd(filePath, tokenInfo);
83+
}
84+
85+
IChangeToken changeToken = tokenInfo.ChangeToken;
86+
if (_pollForChanges)
87+
{
88+
// The expiry of CancellationChangeToken is controlled by this type and consequently we can cache it.
89+
// PollingFileChangeToken on the other hand manages its own lifetime and consequently we cannot cache it.
90+
changeToken = new CompositeFileChangeToken(
91+
new[]
92+
{
93+
changeToken,
94+
new PollingFileChangeToken(new FileInfo(filePath))
95+
});
96+
}
97+
98+
return changeToken;
99+
}
100+
101+
private IChangeToken ResolveFileTokensForGlobbingPattern(string filter)
102+
{
103+
var matcher = new Matcher(StringComparison.OrdinalIgnoreCase);
104+
matcher.AddInclude(filter);
105+
106+
var directoryBase = new DirectoryInfoWrapper(new DirectoryInfo(_root));
107+
var result = matcher.Execute(directoryBase);
108+
109+
var changeTokens = new List<IChangeToken>();
110+
foreach (var file in result.Files)
111+
{
112+
var changeToken = GetOrAddChangeToken(file.Path);
113+
changeTokens.Add(changeToken);
114+
}
115+
116+
return new CompositeFileChangeToken(changeTokens);
117+
}
118+
60119
public void Dispose()
61120
{
62121
_fileWatcher.Dispose();
@@ -89,9 +148,9 @@ private void OnChanged(object sender, FileSystemEventArgs e)
89148
private void OnError(object sender, ErrorEventArgs e)
90149
{
91150
// Notify all cache entries on error.
92-
foreach (var token in _tokenCache.Values)
151+
foreach (var path in _matchInfoCache.Keys)
93152
{
94-
ReportChangeForMatchedEntries(token.Pattern);
153+
ReportChangeForMatchedEntries(path);
95154
}
96155
}
97156

@@ -104,30 +163,23 @@ private void OnFileSystemEntryChange(string fullPath)
104163
}
105164

106165
var relativePath = fullPath.Substring(_root.Length);
107-
if (_tokenCache.ContainsKey(relativePath))
108-
{
109-
ReportChangeForMatchedEntries(relativePath);
110-
}
111-
else
112-
{
113-
foreach (var token in _tokenCache.Values.Where(t => t.IsMatch(relativePath)))
114-
{
115-
ReportChangeForMatchedEntries(token.Pattern);
116-
}
117-
}
166+
ReportChangeForMatchedEntries(relativePath);
118167
}
119168

120-
private void ReportChangeForMatchedEntries(string pattern)
169+
private void ReportChangeForMatchedEntries(string path)
121170
{
122-
FileChangeToken changeToken;
123-
if (_tokenCache.TryRemove(pattern, out changeToken))
171+
path = NormalizePath(path);
172+
173+
ChangeTokenInfo matchInfo;
174+
if (_matchInfoCache.TryRemove(path, out matchInfo))
124175
{
125-
changeToken.Changed();
126-
if (_tokenCache.Count == 0)
176+
CancelToken(matchInfo);
177+
178+
if (_matchInfoCache.Count == 0)
127179
{
128180
lock (_lockObject)
129181
{
130-
if (_tokenCache.Count == 0 && _fileWatcher.EnableRaisingEvents)
182+
if (_matchInfoCache.Count == 0 && _fileWatcher.EnableRaisingEvents)
131183
{
132184
// Perf: Turn off the file monitoring if no files to monitor.
133185
_fileWatcher.EnableRaisingEvents = false;
@@ -137,54 +189,47 @@ private void ReportChangeForMatchedEntries(string pattern)
137189
}
138190
}
139191

140-
private string NormalizeFilter(string filter)
192+
private static string NormalizePath(string filter) => filter = filter.Replace('\\', '/');
193+
194+
private static bool IsDirectoryPath(string path)
195+
{
196+
return path.Length > 0
197+
&& (path[path.Length - 1] == Path.DirectorySeparatorChar || path[path.Length - 1] == Path.AltDirectorySeparatorChar);
198+
}
199+
200+
private static void CancelToken(ChangeTokenInfo matchInfo)
141201
{
142-
// If the searchPath ends with \ or /, we treat searchPath as a directory,
143-
// and will include everything under it, recursively.
144-
if (IsDirectoryPath(filter))
202+
if (matchInfo.TokenSource.IsCancellationRequested)
145203
{
146-
filter = filter + "**" + Path.DirectorySeparatorChar + "*";
204+
return;
147205
}
148206

149-
filter = Path.DirectorySeparatorChar == '/' ?
150-
filter.Replace('\\', Path.DirectorySeparatorChar) :
151-
filter.Replace('/', Path.DirectorySeparatorChar);
152-
153-
return filter;
154-
}
207+
Task.Run(() =>
208+
{
209+
try
210+
{
211+
matchInfo.TokenSource.Cancel();
212+
}
213+
catch
214+
{
155215

156-
private bool IsDirectoryPath(string path)
157-
{
158-
return path != null && path.Length >= 1 && (path[path.Length - 1] == Path.DirectorySeparatorChar || path[path.Length - 1] == Path.AltDirectorySeparatorChar);
216+
}
217+
});
159218
}
160219

161-
private string WildcardToRegexPattern(string wildcard)
220+
private struct ChangeTokenInfo
162221
{
163-
var regex = Regex.Escape(wildcard);
164-
165-
if (Path.DirectorySeparatorChar == '/')
222+
public ChangeTokenInfo(
223+
CancellationTokenSource tokenSource,
224+
CancellationChangeToken changeToken)
166225
{
167-
// regex wildcard adjustments for *nix-style file systems.
168-
regex = regex
169-
.Replace(@"\*\*/", "(.*/)?") //For recursive wildcards /**/, include the current directory.
170-
.Replace(@"\*\*", ".*") // For recursive wildcards that don't end in a slash e.g. **.txt would be treated as a .txt file at any depth
171-
.Replace(@"\*\.\*", @"\*") // "*.*" is equivalent to "*"
172-
.Replace(@"\*", @"[^/]*(/)?") // For non recursive searches, limit it any character that is not a directory separator
173-
.Replace(@"\?", "."); // ? translates to a single any character
174-
}
175-
else
176-
{
177-
// regex wildcard adjustments for Windows-style file systems.
178-
regex = regex
179-
.Replace("/", @"\\") // On Windows, / is treated the same as \.
180-
.Replace(@"\*\*\\", @"(.*\\)?") //For recursive wildcards \**\, include the current directory.
181-
.Replace(@"\*\*", ".*") // For recursive wildcards that don't end in a slash e.g. **.txt would be treated as a .txt file at any depth
182-
.Replace(@"\*\.\*", @"\*") // "*.*" is equivalent to "*"
183-
.Replace(@"\*", @"[^\\]*(\\)?") // For non recursive searches, limit it any character that is not a directory separator
184-
.Replace(@"\?", "."); // ? translates to a single any character
226+
TokenSource = tokenSource;
227+
ChangeToken = changeToken;
185228
}
186229

187-
return regex;
230+
public CancellationTokenSource TokenSource { get; }
231+
232+
public CancellationChangeToken ChangeToken { get; }
188233
}
189234
}
190235
}

0 commit comments

Comments
 (0)