OLD | NEW |
| (Empty) |
1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 // TODO(rnystrom): Merge with mac_os version. | |
5 | |
6 library watcher.directory_watcher.windows; | |
7 | |
8 import 'dart:async'; | |
9 import 'dart:collection'; | |
10 import 'dart:io'; | |
11 | |
12 import 'package:path/path.dart' as p; | |
13 | |
14 import '../constructable_file_system_event.dart'; | |
15 import '../directory_watcher.dart'; | |
16 import '../path_set.dart'; | |
17 import '../resubscribable.dart'; | |
18 import '../utils.dart'; | |
19 import '../watch_event.dart'; | |
20 | |
21 class WindowsDirectoryWatcher extends ResubscribableWatcher | |
22 implements DirectoryWatcher { | |
23 String get directory => path; | |
24 | |
25 WindowsDirectoryWatcher(String directory) | |
26 : super(directory, () => new _WindowsDirectoryWatcher(directory)); | |
27 } | |
28 | |
29 class _EventBatcher { | |
30 static const Duration _BATCH_DELAY = const Duration(milliseconds: 100); | |
31 final List<FileSystemEvent> events = []; | |
32 Timer timer; | |
33 | |
34 void addEvent(FileSystemEvent event, void callback()) { | |
35 events.add(event); | |
36 if (timer != null) { | |
37 timer.cancel(); | |
38 } | |
39 timer = new Timer(_BATCH_DELAY, callback); | |
40 } | |
41 | |
42 void cancelTimer() { | |
43 timer.cancel(); | |
44 } | |
45 } | |
46 | |
47 class _WindowsDirectoryWatcher | |
48 implements DirectoryWatcher, ManuallyClosedWatcher { | |
49 String get directory => path; | |
50 final String path; | |
51 | |
52 Stream<WatchEvent> get events => _eventsController.stream; | |
53 final _eventsController = new StreamController<WatchEvent>.broadcast(); | |
54 | |
55 bool get isReady => _readyCompleter.isCompleted; | |
56 | |
57 Future get ready => _readyCompleter.future; | |
58 final _readyCompleter = new Completer(); | |
59 | |
60 final Map<String, _EventBatcher> _eventBatchers = | |
61 new HashMap<String, _EventBatcher>(); | |
62 | |
63 /// The set of files that are known to exist recursively within the watched | |
64 /// directory. | |
65 /// | |
66 /// The state of files on the filesystem is compared against this to determine | |
67 /// the real change that occurred. This is also used to emit REMOVE events | |
68 /// when subdirectories are moved out of the watched directory. | |
69 final PathSet _files; | |
70 | |
71 /// The subscription to the stream returned by [Directory.watch]. | |
72 StreamSubscription<FileSystemEvent> _watchSubscription; | |
73 | |
74 /// The subscription to the stream returned by [Directory.watch] of the | |
75 /// parent directory to [directory]. This is needed to detect changes to | |
76 /// [directory], as they are not included on Windows. | |
77 StreamSubscription<FileSystemEvent> _parentWatchSubscription; | |
78 | |
79 /// The subscription to the [Directory.list] call for the initial listing of | |
80 /// the directory to determine its initial state. | |
81 StreamSubscription<FileSystemEntity> _initialListSubscription; | |
82 | |
83 /// The subscriptions to the [Directory.list] calls for listing the contents | |
84 /// of subdirectories that were moved into the watched directory. | |
85 final Set<StreamSubscription<FileSystemEntity>> _listSubscriptions | |
86 = new HashSet<StreamSubscription<FileSystemEntity>>(); | |
87 | |
88 _WindowsDirectoryWatcher(String path) | |
89 : path = path, | |
90 _files = new PathSet(path) { | |
91 // Before we're ready to emit events, wait for [_listDir] to complete. | |
92 _listDir().then((_) { | |
93 _startWatch(); | |
94 _startParentWatcher(); | |
95 _readyCompleter.complete(); | |
96 }); | |
97 } | |
98 | |
99 void close() { | |
100 if (_watchSubscription != null) _watchSubscription.cancel(); | |
101 if (_parentWatchSubscription != null) _parentWatchSubscription.cancel(); | |
102 if (_initialListSubscription != null) _initialListSubscription.cancel(); | |
103 for (var sub in _listSubscriptions) { | |
104 sub.cancel(); | |
105 } | |
106 _listSubscriptions.clear(); | |
107 for (var batcher in _eventBatchers.values) { | |
108 batcher.cancelTimer(); | |
109 } | |
110 _eventBatchers.clear(); | |
111 _watchSubscription = null; | |
112 _parentWatchSubscription = null; | |
113 _initialListSubscription = null; | |
114 _eventsController.close(); | |
115 } | |
116 | |
117 /// On Windows, if [directory] is deleted, we will not receive any event. | |
118 /// | |
119 /// Instead, we add a watcher on the parent folder (if any), that can notify | |
120 /// us about [path]. This also includes events such as moves. | |
121 void _startParentWatcher() { | |
122 var absoluteDir = p.absolute(path); | |
123 var parent = p.dirname(absoluteDir); | |
124 // Check if [path] is already the root directory. | |
125 if (FileSystemEntity.identicalSync(parent, path)) return; | |
126 var parentStream = new Directory(parent).watch(recursive: false); | |
127 _parentWatchSubscription = parentStream.listen((event) { | |
128 // Only look at events for 'directory'. | |
129 if (p.basename(event.path) != p.basename(absoluteDir)) return; | |
130 // Test if the directory is removed. FileSystemEntity.typeSync will | |
131 // return NOT_FOUND if it's unable to decide upon the type, including | |
132 // access denied issues, which may happen when the directory is deleted. | |
133 // FileSystemMoveEvent and FileSystemDeleteEvent events will always mean | |
134 // the directory is now gone. | |
135 if (event is FileSystemMoveEvent || | |
136 event is FileSystemDeleteEvent || | |
137 (FileSystemEntity.typeSync(path) == | |
138 FileSystemEntityType.NOT_FOUND)) { | |
139 for (var path in _files.toSet()) { | |
140 _emitEvent(ChangeType.REMOVE, path); | |
141 } | |
142 _files.clear(); | |
143 close(); | |
144 } | |
145 }, onError: (error) { | |
146 // Ignore errors, simply close the stream. The user listens on | |
147 // [directory], and while it can fail to listen on the parent, we may | |
148 // still be able to listen on the path requested. | |
149 _parentWatchSubscription.cancel(); | |
150 _parentWatchSubscription = null; | |
151 }); | |
152 } | |
153 | |
154 void _onEvent(FileSystemEvent event) { | |
155 assert(isReady); | |
156 final batcher = _eventBatchers.putIfAbsent( | |
157 event.path, () => new _EventBatcher()); | |
158 batcher.addEvent(event, () { | |
159 _eventBatchers.remove(event.path); | |
160 _onBatch(batcher.events); | |
161 }); | |
162 } | |
163 | |
164 /// The callback that's run when [Directory.watch] emits a batch of events. | |
165 void _onBatch(List<FileSystemEvent> batch) { | |
166 _sortEvents(batch).forEach((path, events) { | |
167 | |
168 var canonicalEvent = _canonicalEvent(events); | |
169 events = canonicalEvent == null ? | |
170 _eventsBasedOnFileSystem(path) : [canonicalEvent]; | |
171 | |
172 for (var event in events) { | |
173 if (event is FileSystemCreateEvent) { | |
174 if (!event.isDirectory) { | |
175 if (_files.contains(path)) continue; | |
176 | |
177 _emitEvent(ChangeType.ADD, path); | |
178 _files.add(path); | |
179 continue; | |
180 } | |
181 | |
182 if (_files.containsDir(path)) continue; | |
183 | |
184 var stream = new Directory(path).list(recursive: true); | |
185 var sub; | |
186 sub = stream.listen((entity) { | |
187 if (entity is Directory) return; | |
188 if (_files.contains(path)) return; | |
189 | |
190 _emitEvent(ChangeType.ADD, entity.path); | |
191 _files.add(entity.path); | |
192 }, onDone: () { | |
193 _listSubscriptions.remove(sub); | |
194 }, onError: (e, stackTrace) { | |
195 _listSubscriptions.remove(sub); | |
196 _emitError(e, stackTrace); | |
197 }, cancelOnError: true); | |
198 _listSubscriptions.add(sub); | |
199 } else if (event is FileSystemModifyEvent) { | |
200 if (!event.isDirectory) { | |
201 _emitEvent(ChangeType.MODIFY, path); | |
202 } | |
203 } else { | |
204 assert(event is FileSystemDeleteEvent); | |
205 for (var removedPath in _files.remove(path)) { | |
206 _emitEvent(ChangeType.REMOVE, removedPath); | |
207 } | |
208 } | |
209 } | |
210 }); | |
211 } | |
212 | |
213 /// Sort all the events in a batch into sets based on their path. | |
214 /// | |
215 /// A single input event may result in multiple events in the returned map; | |
216 /// for example, a MOVE event becomes a DELETE event for the source and a | |
217 /// CREATE event for the destination. | |
218 /// | |
219 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it | |
220 /// contain any events relating to [path]. | |
221 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) { | |
222 var eventsForPaths = {}; | |
223 | |
224 // Events within directories that already have events are superfluous; the | |
225 // directory's full contents will be examined anyway, so we ignore such | |
226 // events. Emitting them could cause useless or out-of-order events. | |
227 var directories = unionAll(batch.map((event) { | |
228 if (!event.isDirectory) return new Set(); | |
229 if (event is! FileSystemMoveEvent) return new Set.from([event.path]); | |
230 return new Set.from([event.path, event.destination]); | |
231 })); | |
232 | |
233 isInModifiedDirectory(path) => | |
234 directories.any((dir) => path != dir && path.startsWith(dir)); | |
235 | |
236 addEvent(path, event) { | |
237 if (isInModifiedDirectory(path)) return; | |
238 var set = eventsForPaths.putIfAbsent(path, () => new Set()); | |
239 set.add(event); | |
240 } | |
241 | |
242 for (var event in batch) { | |
243 if (event is FileSystemMoveEvent) { | |
244 addEvent(event.destination, event); | |
245 } | |
246 addEvent(event.path, event); | |
247 } | |
248 | |
249 return eventsForPaths; | |
250 } | |
251 | |
252 /// Returns the canonical event from a batch of events on the same path, if | |
253 /// one exists. | |
254 /// | |
255 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and | |
256 /// CREATE, or events with different values for [isDirectory]), this returns a | |
257 /// single event that describes what happened to the path in question. | |
258 /// | |
259 /// If [batch] does contain contradictory events, this returns `null` to | |
260 /// indicate that the state of the path on the filesystem should be checked to | |
261 /// determine what occurred. | |
262 FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) { | |
263 // An empty batch indicates that we've learned earlier that the batch is | |
264 // contradictory (e.g. because of a move). | |
265 if (batch.isEmpty) return null; | |
266 | |
267 var type = batch.first.type; | |
268 var isDir = batch.first.isDirectory; | |
269 | |
270 for (var event in batch.skip(1)) { | |
271 // If one event reports that the file is a directory and another event | |
272 // doesn't, that's a contradiction. | |
273 if (isDir != event.isDirectory) return null; | |
274 | |
275 // Modify events don't contradict either CREATE or REMOVE events. We can | |
276 // safely assume the file was modified after a CREATE or before the | |
277 // REMOVE; otherwise there will also be a REMOVE or CREATE event | |
278 // (respectively) that will be contradictory. | |
279 if (event is FileSystemModifyEvent) continue; | |
280 assert(event is FileSystemCreateEvent || | |
281 event is FileSystemDeleteEvent || | |
282 event is FileSystemMoveEvent); | |
283 | |
284 // If we previously thought this was a MODIFY, we now consider it to be a | |
285 // CREATE or REMOVE event. This is safe for the same reason as above. | |
286 if (type == FileSystemEvent.MODIFY) { | |
287 type = event.type; | |
288 continue; | |
289 } | |
290 | |
291 // A CREATE event contradicts a REMOVE event and vice versa. | |
292 assert(type == FileSystemEvent.CREATE || | |
293 type == FileSystemEvent.DELETE || | |
294 type == FileSystemEvent.MOVE); | |
295 if (type != event.type) return null; | |
296 } | |
297 | |
298 switch (type) { | |
299 case FileSystemEvent.CREATE: | |
300 return new ConstructableFileSystemCreateEvent(batch.first.path, isDir); | |
301 case FileSystemEvent.DELETE: | |
302 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir); | |
303 case FileSystemEvent.MODIFY: | |
304 return new ConstructableFileSystemModifyEvent( | |
305 batch.first.path, isDir, false); | |
306 case FileSystemEvent.MOVE: | |
307 return null; | |
308 default: throw 'unreachable'; | |
309 } | |
310 } | |
311 | |
312 /// Returns one or more events that describe the change between the last known | |
313 /// state of [path] and its current state on the filesystem. | |
314 /// | |
315 /// This returns a list whose order should be reflected in the events emitted | |
316 /// to the user, unlike the batched events from [Directory.watch]. The | |
317 /// returned list may be empty, indicating that no changes occurred to [path] | |
318 /// (probably indicating that it was created and then immediately deleted). | |
319 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) { | |
320 var fileExisted = _files.contains(path); | |
321 var dirExisted = _files.containsDir(path); | |
322 var fileExists = new File(path).existsSync(); | |
323 var dirExists = new Directory(path).existsSync(); | |
324 | |
325 var events = []; | |
326 if (fileExisted) { | |
327 if (fileExists) { | |
328 events.add(new ConstructableFileSystemModifyEvent(path, false, false)); | |
329 } else { | |
330 events.add(new ConstructableFileSystemDeleteEvent(path, false)); | |
331 } | |
332 } else if (dirExisted) { | |
333 if (dirExists) { | |
334 // If we got contradictory events for a directory that used to exist and | |
335 // still exists, we need to rescan the whole thing in case it was | |
336 // replaced with a different directory. | |
337 events.add(new ConstructableFileSystemDeleteEvent(path, true)); | |
338 events.add(new ConstructableFileSystemCreateEvent(path, true)); | |
339 } else { | |
340 events.add(new ConstructableFileSystemDeleteEvent(path, true)); | |
341 } | |
342 } | |
343 | |
344 if (!fileExisted && fileExists) { | |
345 events.add(new ConstructableFileSystemCreateEvent(path, false)); | |
346 } else if (!dirExisted && dirExists) { | |
347 events.add(new ConstructableFileSystemCreateEvent(path, true)); | |
348 } | |
349 | |
350 return events; | |
351 } | |
352 | |
353 /// The callback that's run when the [Directory.watch] stream is closed. | |
354 /// Note that this is unlikely to happen on Windows, unless the system itself | |
355 /// closes the handle. | |
356 void _onDone() { | |
357 _watchSubscription = null; | |
358 | |
359 // Emit remove events for any remaining files. | |
360 for (var file in _files.toSet()) { | |
361 _emitEvent(ChangeType.REMOVE, file); | |
362 } | |
363 _files.clear(); | |
364 close(); | |
365 } | |
366 | |
367 /// Start or restart the underlying [Directory.watch] stream. | |
368 void _startWatch() { | |
369 // Batch the events together so that we can dedup events. | |
370 var innerStream = new Directory(path).watch(recursive: true); | |
371 _watchSubscription = innerStream.listen(_onEvent, | |
372 onError: _eventsController.addError, | |
373 onDone: _onDone); | |
374 } | |
375 | |
376 /// Starts or restarts listing the watched directory to get an initial picture | |
377 /// of its state. | |
378 Future _listDir() { | |
379 assert(!isReady); | |
380 if (_initialListSubscription != null) _initialListSubscription.cancel(); | |
381 | |
382 _files.clear(); | |
383 var completer = new Completer(); | |
384 var stream = new Directory(path).list(recursive: true); | |
385 void handleEntity(entity) { | |
386 if (entity is! Directory) _files.add(entity.path); | |
387 } | |
388 _initialListSubscription = stream.listen( | |
389 handleEntity, | |
390 onError: _emitError, | |
391 onDone: completer.complete, | |
392 cancelOnError: true); | |
393 return completer.future; | |
394 } | |
395 | |
396 /// Emit an event with the given [type] and [path]. | |
397 void _emitEvent(ChangeType type, String path) { | |
398 if (!isReady) return; | |
399 | |
400 _eventsController.add(new WatchEvent(type, path)); | |
401 } | |
402 | |
403 /// Emit an error, then close the watcher. | |
404 void _emitError(error, StackTrace stackTrace) { | |
405 _eventsController.addError(error, stackTrace); | |
406 close(); | |
407 } | |
408 } | |
OLD | NEW |