forked from microsoft/qsharp-compiler
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathFileSystemWatcher.cs
265 lines (225 loc) · 12.6 KB
/
FileSystemWatcher.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Permissions;
using System.Threading.Tasks;
using Microsoft.Quantum.QsCompiler;
using Microsoft.Quantum.QsCompiler.CompilationBuilder;
using Microsoft.VisualStudio.LanguageServer.Protocol;
namespace Microsoft.Quantum.QsLanguageServer
{
/// <summary>
/// This class provides a basic file watcher for the LSP that sends notifications on watched files
/// when they change on disk (i.e. are added, removed, or edited).
/// The implemeneted mechanism is far from perfect and will fail in some cases -
/// in particular I expect it to fail (silently) for autogenerated file system edits generated in rapid succession,
/// especially in combination with large batch edits in the file system.
/// However, all in all splitting out the logic to generate notifications for individual files here (even if it is less than perfect),
/// seems better and overall less error prone than having to deal with less abstraction on the server side.
/// I am fully aware that this mechanism is not a neat solution, and it should probably be revised at some point in the future.
/// </summary>
internal class FileWatcher
{
private readonly ConcurrentBag<System.IO.FileSystemWatcher> Watchers;
private readonly Action<Exception> OnException;
private void OnBufferOverflow(object sender, ErrorEventArgs e)
{
// Todo: We should at some point implement a mechanism to try and recover from buffer overflows in the file watcher.
try { QsCompilerError.Raise($"buffer overflow in file system watcher: \n{e.GetException()}"); }
catch (Exception ex) { this.OnException(ex); }
}
/// <summary>
/// the keys contain the *absolute* uri to a folder, and the values are the set of the *relative* names of all contained files and folders
/// </summary>
private readonly Dictionary<Uri, ImmutableHashSet<string>> WatchedDirectories;
private readonly ConcurrentDictionary<Uri, IEnumerable<string>> GlobPatterns;
private readonly ProcessingQueue Processing;
public event FileEventHandler FileEvent;
public delegate void FileEventHandler(FileEvent e);
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
public FileWatcher(Action<Exception> onException = null)
{
this.OnException = onException ?? throw new ArgumentNullException(nameof(onException));
this.Watchers = new ConcurrentBag<System.IO.FileSystemWatcher>();
this.WatchedDirectories = new Dictionary<Uri, ImmutableHashSet<string>>();
this.Processing = new ProcessingQueue(this.OnException, "error in file system watcher") ;
this.GlobPatterns = new ConcurrentDictionary<Uri, IEnumerable<string>>();
}
/// <summary>
/// Returns a file system watcher for the given folder and pattern, with the proper event handlers added.
/// IMPORTANT: The returned watcher is disabled and needs to be enabled by setting EnableRaisingEvents to true.
/// </summary>
private System.IO.FileSystemWatcher GetWatcher(string folder, string pattern, NotifyFilters notifyOn)
{
var watcher = new System.IO.FileSystemWatcher
{
NotifyFilter = notifyOn,
Filter = pattern,
Path = folder,
// An entry is the buffer is 12 bytes plus the length of the file path times two.
// The default buffer size is 2*4096, which is around 15 events.
InternalBufferSize = 16 * 4096,
};
watcher.Error += new ErrorEventHandler(this.OnBufferOverflow);
watcher.Renamed += new RenamedEventHandler(this.OnRenamed);
watcher.Changed += new FileSystemEventHandler(this.OnChanged);
watcher.Created += new FileSystemEventHandler(this.OnCreated);
watcher.Deleted += new FileSystemEventHandler(this.OnDeleted);
return watcher;
}
/// <summary>
/// Initializes the given dictionary with the structure of the given directory as it is currently on disk for the given glob pattern.
/// Returns true if the routine succeeded without throwing an exception and false otherwise.
/// Returns true without doing anything if no directory exists at the given (absolute!) path.
/// </summary>
private static bool GlobDirectoryStructure(Dictionary<Uri, ImmutableHashSet<string>> directories, string path, IEnumerable<string> globPatterns)
{
if (!Directory.Exists(path)) return true; // successfully completed, but nothing to be done
var root = Uri.TryCreate(path, UriKind.Absolute, out Uri uri) ? uri : null;
var success = Directory.EnumerateDirectories(root.LocalPath).TryEnumerate(out var subfolders);
success = globPatterns.TryEnumerate(
pattern => Directory.EnumerateFiles(root.LocalPath, pattern, SearchOption.TopDirectoryOnly), out var files)
&& success;
directories[root] = subfolders.Concat(files.SelectMany(items => items)).Select(Path.GetFileName).ToImmutableHashSet();
foreach (var subfolder in subfolders)
{ success = GlobDirectoryStructure(directories, subfolder, globPatterns) && success; }
return success;
}
/// <summary>
/// Adds suitable listeners to capture all given glob patterns for the given folder,
/// and - if subfolders is set to true - all its subfolders.
/// Does nothing if no folder with the give path exists.
/// </summary>
public Task ListenAsync(string folder, bool subfolders, Action<ImmutableDictionary<Uri, ImmutableHashSet<string>>> onInitialState, params string[] globPatterns)
{
if (!Directory.Exists(folder)) return Task.CompletedTask;
folder = folder.TrimEnd(Path.DirectorySeparatorChar);
globPatterns = globPatterns.Distinct().ToArray();
this.GlobPatterns.AddOrUpdate(new Uri(folder), globPatterns, (_, currentPatterns) => currentPatterns.Concat(globPatterns).Distinct().ToArray());
var filters = globPatterns.Select(p => (p, NotifyFilters.FileName | NotifyFilters.LastWrite));
if (subfolders) filters = filters.Concat(new (string, NotifyFilters)[] { (String.Empty, NotifyFilters.DirectoryName) });
return this.Processing.QueueForExecutionAsync(() =>
{
var dictionary = new Dictionary<Uri, ImmutableHashSet<string>>();
if (subfolders && GlobDirectoryStructure(dictionary, folder, globPatterns))
{
foreach (var entry in dictionary)
{
var current = this.WatchedDirectories.TryGetValue(entry.Key, out ImmutableHashSet<string> c) ? c : ImmutableHashSet<string>.Empty;
this.WatchedDirectories[entry.Key] = current.Union(entry.Value);
}
onInitialState?.Invoke(dictionary.ToImmutableDictionary());
}
foreach (var (pattern, notifyOn) in filters)
{
var watcher = this.GetWatcher(folder, pattern, notifyOn);
watcher.IncludeSubdirectories = subfolders;
watcher.EnableRaisingEvents = true;
this.Watchers.Add(watcher);
}
});
}
// routines called upon creation
private void OnCreatedFile(string fullPath)
{
this.FileEvent?.Invoke(new FileEvent
{
Uri = new Uri(fullPath),
FileChangeType = FileChangeType.Created
});
var dir = new Uri(Path.GetDirectoryName(fullPath));
var current = this.WatchedDirectories.TryGetValue(dir, out ImmutableHashSet<string> items) ? items : ImmutableHashSet<string>.Empty;
this.WatchedDirectories[dir] = current.Add(Path.GetFileName(fullPath));
}
private void RecurCreated(string fullPath, IDictionary<Uri, ImmutableHashSet<string>> newDirectories)
{
var dir = new Uri(fullPath);
if (newDirectories.TryGetValue(dir, out ImmutableHashSet<string> items))
{
var current = this.WatchedDirectories.TryGetValue(dir, out ImmutableHashSet<string> c) ? c : ImmutableHashSet<string>.Empty;
this.WatchedDirectories[dir] = current.Union(items);
foreach (var item in items)
{ this.RecurCreated(Path.Combine(fullPath, item), newDirectories); }
}
else this.OnCreatedFile(fullPath);
}
// Todo: this routine in particular illustrates the limitations of the current mechanism.
public void OnCreated(object source, FileSystemEventArgs e)
{
var directories = new Dictionary<Uri, ImmutableHashSet<string>>();
if (source is System.IO.FileSystemWatcher watcher &&
this.GlobPatterns.TryGetValue(new Uri(watcher.Path), out IEnumerable<string> globPatterns))
{
var maxNrTries = 10; // copied directories need some time until they are on disk -> todo: better solution?
while (maxNrTries-- > 0 && !GlobDirectoryStructure(directories, e.FullPath, globPatterns))
{
directories = new Dictionary<Uri, ImmutableHashSet<string>>();
System.Threading.Thread.Sleep(1000);
}
}
_ = this.Processing.QueueForExecutionAsync(() => RecurCreated(e.FullPath, directories));
}
// routines called upon deletion
private void OnDeletedFile(string fullPath)
{
this.FileEvent?.Invoke(new FileEvent
{
Uri = new Uri(fullPath),
FileChangeType = FileChangeType.Deleted
});
var dir = new Uri(Path.GetDirectoryName(fullPath));
var knownDir = this.WatchedDirectories.TryGetValue(dir, out ImmutableHashSet<string> items);
if (knownDir) this.WatchedDirectories[dir] = items.Remove(Path.GetFileName(fullPath));
}
private void RecurDeleted(string fullPath)
{
if (this.WatchedDirectories.TryGetValue(new Uri(fullPath), out ImmutableHashSet<string> items))
{
foreach (var item in items)
{ this.RecurDeleted(Path.Combine(fullPath, item)); }
this.WatchedDirectories.Remove(new Uri(fullPath));
}
else this.OnDeletedFile(fullPath);
}
public void OnDeleted(object source, FileSystemEventArgs e) =>
this.Processing.QueueForExecutionAsync(() => RecurDeleted(e.FullPath));
// routines called upon changing
private void OnChangedFile(string fullPath) =>
this.FileEvent?.Invoke(new FileEvent
{
Uri = new Uri(fullPath),
FileChangeType = FileChangeType.Changed
});
private void RecurChanged(string fullPath)
{
if (this.WatchedDirectories.TryGetValue(new Uri(fullPath), out var _)) { } // nothing to do here
else this.OnChangedFile(fullPath);
}
public void OnChanged(object source, FileSystemEventArgs e) =>
this.Processing.QueueForExecutionAsync(() => RecurChanged(e.FullPath));
// routines called upon renaming
private void OnRenamedFile(string fullPath, string oldFullPath)
{
this.OnDeletedFile(oldFullPath);
this.OnCreatedFile(fullPath);
}
private void RecurRenamed(string fullPath, string oldFullPath)
{
if (this.WatchedDirectories.TryGetValue(new Uri(oldFullPath), out ImmutableHashSet<string> items))
{
this.WatchedDirectories[new Uri(fullPath)] = items;
foreach (var item in items)
{ this.RecurRenamed(Path.Combine(fullPath, item), Path.Combine(oldFullPath, item)); }
this.WatchedDirectories.Remove(new Uri(oldFullPath));
}
else this.OnRenamedFile(fullPath, oldFullPath);
}
public void OnRenamed(object source, RenamedEventArgs e) =>
this.Processing.QueueForExecutionAsync(() => RecurRenamed(e.FullPath, e.OldFullPath));
}
}