3
3
4
4
using System ;
5
5
using System . Collections . Concurrent ;
6
+ using System . Collections . Generic ;
6
7
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 ;
9
12
using Microsoft . Extensions . Primitives ;
10
13
11
14
namespace Microsoft . Extensions . FileProviders . Physical
12
15
{
13
16
public class PhysicalFilesWatcher : IDisposable
14
17
{
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 ) ;
17
20
private readonly FileSystemWatcher _fileWatcher ;
18
21
private readonly object _lockObject = new object ( ) ;
19
22
private readonly string _root ;
23
+ private readonly bool _pollForChanges ;
20
24
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 )
27
29
{
28
30
_root = root ;
29
31
_fileWatcher = fileSystemWatcher ;
@@ -33,30 +35,87 @@ public PhysicalFilesWatcher(string root, FileSystemWatcher fileSystemWatcher)
33
35
_fileWatcher . Renamed += OnRenamed ;
34
36
_fileWatcher . Deleted += OnChanged ;
35
37
_fileWatcher . Error += OnError ;
38
+
39
+ _pollForChanges = pollForChanges ;
36
40
}
37
41
38
- internal IChangeToken CreateFileChangeToken ( string filter )
42
+ public IChangeToken CreateFileChangeToken ( string filter )
39
43
{
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
+ }
42
61
43
- FileChangeToken changeToken ;
44
- if ( ! _tokenCache . TryGetValue ( pattern , out changeToken ) )
62
+ lock ( _lockObject )
45
63
{
46
- changeToken = _tokenCache . GetOrAdd ( pattern , new FileChangeToken ( pattern ) ) ;
47
- lock ( _lockObject )
64
+ if ( _matchInfoCache . Count > 0 && ! _fileWatcher . EnableRaisingEvents )
48
65
{
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 ;
54
68
}
55
69
}
56
70
57
71
return changeToken ;
58
72
}
59
73
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
+
60
119
public void Dispose ( )
61
120
{
62
121
_fileWatcher . Dispose ( ) ;
@@ -89,9 +148,9 @@ private void OnChanged(object sender, FileSystemEventArgs e)
89
148
private void OnError ( object sender , ErrorEventArgs e )
90
149
{
91
150
// Notify all cache entries on error.
92
- foreach ( var token in _tokenCache . Values )
151
+ foreach ( var path in _matchInfoCache . Keys )
93
152
{
94
- ReportChangeForMatchedEntries ( token . Pattern ) ;
153
+ ReportChangeForMatchedEntries ( path ) ;
95
154
}
96
155
}
97
156
@@ -104,30 +163,23 @@ private void OnFileSystemEntryChange(string fullPath)
104
163
}
105
164
106
165
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 ) ;
118
167
}
119
168
120
- private void ReportChangeForMatchedEntries ( string pattern )
169
+ private void ReportChangeForMatchedEntries ( string path )
121
170
{
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 ) )
124
175
{
125
- changeToken . Changed ( ) ;
126
- if ( _tokenCache . Count == 0 )
176
+ CancelToken ( matchInfo ) ;
177
+
178
+ if ( _matchInfoCache . Count == 0 )
127
179
{
128
180
lock ( _lockObject )
129
181
{
130
- if ( _tokenCache . Count == 0 && _fileWatcher . EnableRaisingEvents )
182
+ if ( _matchInfoCache . Count == 0 && _fileWatcher . EnableRaisingEvents )
131
183
{
132
184
// Perf: Turn off the file monitoring if no files to monitor.
133
185
_fileWatcher . EnableRaisingEvents = false ;
@@ -137,54 +189,47 @@ private void ReportChangeForMatchedEntries(string pattern)
137
189
}
138
190
}
139
191
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 )
141
201
{
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 )
145
203
{
146
- filter = filter + "**" + Path . DirectorySeparatorChar + "*" ;
204
+ return ;
147
205
}
148
206
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
+ {
155
215
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
+ } ) ;
159
218
}
160
219
161
- private string WildcardToRegexPattern ( string wildcard )
220
+ private struct ChangeTokenInfo
162
221
{
163
- var regex = Regex . Escape ( wildcard ) ;
164
-
165
- if ( Path . DirectorySeparatorChar == '/' )
222
+ public ChangeTokenInfo (
223
+ CancellationTokenSource tokenSource ,
224
+ CancellationChangeToken changeToken )
166
225
{
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 ;
185
228
}
186
229
187
- return regex ;
230
+ public CancellationTokenSource TokenSource { get ; }
231
+
232
+ public CancellationChangeToken ChangeToken { get ; }
188
233
}
189
234
}
190
235
}
0 commit comments