Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(660)

Side by Side Diff: observatory_pub_packages/watcher/src/directory_watcher/windows.dart

Issue 816693004: Add observatory_pub_packages snapshot to third_party (Closed) Base URL: http://dart.googlecode.com/svn/third_party/
Patch Set: Created 6 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 import 'package:stack_trace/stack_trace.dart';
14
15 import '../constructable_file_system_event.dart';
16 import '../path_set.dart';
17 import '../utils.dart';
18 import '../watch_event.dart';
19 import 'resubscribable.dart';
20
21 class WindowsDirectoryWatcher extends ResubscribableDirectoryWatcher {
22 WindowsDirectoryWatcher(String directory)
23 : super(directory, () => new _WindowsDirectoryWatcher(directory));
24 }
25
26 class _EventBatcher {
27 static const Duration _BATCH_DELAY = const Duration(milliseconds: 100);
28 final List<FileSystemEvent> events = [];
29 Timer timer;
30
31 void addEvent(FileSystemEvent event, void callback()) {
32 events.add(event);
33 if (timer != null) {
34 timer.cancel();
35 }
36 timer = new Timer(_BATCH_DELAY, callback);
37 }
38
39 void cancelTimer() {
40 timer.cancel();
41 }
42 }
43
44 class _WindowsDirectoryWatcher implements ManuallyClosedDirectoryWatcher {
45 final String directory;
46
47 Stream<WatchEvent> get events => _eventsController.stream;
48 final _eventsController = new StreamController<WatchEvent>.broadcast();
49
50 bool get isReady => _readyCompleter.isCompleted;
51
52 Future get ready => _readyCompleter.future;
53 final _readyCompleter = new Completer();
54
55 final Map<String, _EventBatcher> _eventBatchers =
56 new HashMap<String, _EventBatcher>();
57
58 /// The set of files that are known to exist recursively within the watched
59 /// directory.
60 ///
61 /// The state of files on the filesystem is compared against this to determine
62 /// the real change that occurred. This is also used to emit REMOVE events
63 /// when subdirectories are moved out of the watched directory.
64 final PathSet _files;
65
66 /// The subscription to the stream returned by [Directory.watch].
67 StreamSubscription<FileSystemEvent> _watchSubscription;
68
69 /// The subscription to the stream returned by [Directory.watch] of the
70 /// parent directory to [directory]. This is needed to detect changes to
71 /// [directory], as they are not included on Windows.
72 StreamSubscription<FileSystemEvent> _parentWatchSubscription;
73
74 /// The subscription to the [Directory.list] call for the initial listing of
75 /// the directory to determine its initial state.
76 StreamSubscription<FileSystemEntity> _initialListSubscription;
77
78 /// The subscriptions to the [Directory.list] calls for listing the contents
79 /// of subdirectories that were moved into the watched directory.
80 final Set<StreamSubscription<FileSystemEntity>> _listSubscriptions
81 = new HashSet<StreamSubscription<FileSystemEntity>>();
82
83 _WindowsDirectoryWatcher(String directory)
84 : directory = directory, _files = new PathSet(directory) {
85 // Before we're ready to emit events, wait for [_listDir] to complete.
86 _listDir().then((_) {
87 _startWatch();
88 _startParentWatcher();
89 _readyCompleter.complete();
90 });
91 }
92
93 void close() {
94 if (_watchSubscription != null) _watchSubscription.cancel();
95 if (_parentWatchSubscription != null) _parentWatchSubscription.cancel();
96 if (_initialListSubscription != null) _initialListSubscription.cancel();
97 for (var sub in _listSubscriptions) {
98 sub.cancel();
99 }
100 _listSubscriptions.clear();
101 for (var batcher in _eventBatchers.values) {
102 batcher.cancelTimer();
103 }
104 _eventBatchers.clear();
105 _watchSubscription = null;
106 _parentWatchSubscription = null;
107 _initialListSubscription = null;
108 _eventsController.close();
109 }
110
111 /// On Windows, if [directory] is deleted, we will not receive any event.
112 ///
113 /// Instead, we add a watcher on the parent folder (if any), that can notify
114 /// us about [directory]. This also includes events such as moves.
115 void _startParentWatcher() {
116 var absoluteDir = p.absolute(directory);
117 var parent = p.dirname(absoluteDir);
118 // Check if [directory] is already the root directory.
119 if (FileSystemEntity.identicalSync(parent, directory)) return;
120 var parentStream = Chain.track(
121 new Directory(parent).watch(recursive: false));
122 _parentWatchSubscription = parentStream.listen((event) {
123 // Only look at events for 'directory'.
124 if (p.basename(event.path) != p.basename(absoluteDir)) return;
125 // Test if the directory is removed. FileSystemEntity.typeSync will
126 // return NOT_FOUND if it's unable to decide upon the type, including
127 // access denied issues, which may happen when the directory is deleted.
128 // FileSystemMoveEvent and FileSystemDeleteEvent events will always mean
129 // the directory is now gone.
130 if (event is FileSystemMoveEvent ||
131 event is FileSystemDeleteEvent ||
132 (FileSystemEntity.typeSync(directory) ==
133 FileSystemEntityType.NOT_FOUND)) {
134 for (var path in _files.toSet()) {
135 _emitEvent(ChangeType.REMOVE, path);
136 }
137 _files.clear();
138 close();
139 }
140 }, onError: (error) {
141 // Ignore errors, simply close the stream. The user listens on
142 // [directory], and while it can fail to listen on the parent, we may
143 // still be able to listen on the path requested.
144 _parentWatchSubscription.cancel();
145 _parentWatchSubscription = null;
146 });
147 }
148
149 void _onEvent(FileSystemEvent event) {
150 assert(isReady);
151 final batcher = _eventBatchers.putIfAbsent(
152 event.path, () => new _EventBatcher());
153 batcher.addEvent(event, () {
154 _eventBatchers.remove(event.path);
155 _onBatch(batcher.events);
156 });
157 }
158
159 /// The callback that's run when [Directory.watch] emits a batch of events.
160 void _onBatch(List<FileSystemEvent> batch) {
161 _sortEvents(batch).forEach((path, events) {
162 var relativePath = p.relative(path, from: directory);
163
164 var canonicalEvent = _canonicalEvent(events);
165 events = canonicalEvent == null ?
166 _eventsBasedOnFileSystem(path) : [canonicalEvent];
167
168 for (var event in events) {
169 if (event is FileSystemCreateEvent) {
170 if (!event.isDirectory) {
171 if (_files.contains(path)) continue;
172
173 _emitEvent(ChangeType.ADD, path);
174 _files.add(path);
175 continue;
176 }
177
178 if (_files.containsDir(path)) continue;
179
180 var stream = Chain.track(new Directory(path).list(recursive: true));
181 var sub;
182 sub = stream.listen((entity) {
183 if (entity is Directory) return;
184 if (_files.contains(path)) return;
185
186 _emitEvent(ChangeType.ADD, entity.path);
187 _files.add(entity.path);
188 }, onDone: () {
189 _listSubscriptions.remove(sub);
190 }, onError: (e, stackTrace) {
191 _listSubscriptions.remove(sub);
192 _emitError(e, stackTrace);
193 }, cancelOnError: true);
194 _listSubscriptions.add(sub);
195 } else if (event is FileSystemModifyEvent) {
196 if (!event.isDirectory) {
197 _emitEvent(ChangeType.MODIFY, path);
198 }
199 } else {
200 assert(event is FileSystemDeleteEvent);
201 for (var removedPath in _files.remove(path)) {
202 _emitEvent(ChangeType.REMOVE, removedPath);
203 }
204 }
205 }
206 });
207 }
208
209 /// Sort all the events in a batch into sets based on their path.
210 ///
211 /// A single input event may result in multiple events in the returned map;
212 /// for example, a MOVE event becomes a DELETE event for the source and a
213 /// CREATE event for the destination.
214 ///
215 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
216 /// contain any events relating to [directory].
217 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
218 var eventsForPaths = {};
219
220 // Events within directories that already have events are superfluous; the
221 // directory's full contents will be examined anyway, so we ignore such
222 // events. Emitting them could cause useless or out-of-order events.
223 var directories = unionAll(batch.map((event) {
224 if (!event.isDirectory) return new Set();
225 if (event is! FileSystemMoveEvent) return new Set.from([event.path]);
226 return new Set.from([event.path, event.destination]);
227 }));
228
229 isInModifiedDirectory(path) =>
230 directories.any((dir) => path != dir && path.startsWith(dir));
231
232 addEvent(path, event) {
233 if (isInModifiedDirectory(path)) return;
234 var set = eventsForPaths.putIfAbsent(path, () => new Set());
235 set.add(event);
236 }
237
238 for (var event in batch) {
239 if (event is FileSystemMoveEvent) {
240 addEvent(event.destination, event);
241 }
242 addEvent(event.path, event);
243 }
244
245 return eventsForPaths;
246 }
247
248 /// Returns the canonical event from a batch of events on the same path, if
249 /// one exists.
250 ///
251 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
252 /// CREATE, or events with different values for [isDirectory]), this returns a
253 /// single event that describes what happened to the path in question.
254 ///
255 /// If [batch] does contain contradictory events, this returns `null` to
256 /// indicate that the state of the path on the filesystem should be checked to
257 /// determine what occurred.
258 FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) {
259 // An empty batch indicates that we've learned earlier that the batch is
260 // contradictory (e.g. because of a move).
261 if (batch.isEmpty) return null;
262
263 var type = batch.first.type;
264 var isDir = batch.first.isDirectory;
265 var hadModifyEvent = false;
266
267 for (var event in batch.skip(1)) {
268 // If one event reports that the file is a directory and another event
269 // doesn't, that's a contradiction.
270 if (isDir != event.isDirectory) return null;
271
272 // Modify events don't contradict either CREATE or REMOVE events. We can
273 // safely assume the file was modified after a CREATE or before the
274 // REMOVE; otherwise there will also be a REMOVE or CREATE event
275 // (respectively) that will be contradictory.
276 if (event is FileSystemModifyEvent) {
277 hadModifyEvent = true;
278 continue;
279 }
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: assert(false);
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 =
371 Chain.track(new Directory(directory).watch(recursive: true));
372 _watchSubscription = innerStream.listen(_onEvent,
373 onError: _eventsController.addError,
374 onDone: _onDone);
375 }
376
377 /// Starts or restarts listing the watched directory to get an initial picture
378 /// of its state.
379 Future _listDir() {
380 assert(!isReady);
381 if (_initialListSubscription != null) _initialListSubscription.cancel();
382
383 _files.clear();
384 var completer = new Completer();
385 var stream = Chain.track(new Directory(directory).list(recursive: true));
386 void handleEntity(entity) {
387 if (entity is! Directory) _files.add(entity.path);
388 }
389 _initialListSubscription = stream.listen(
390 handleEntity,
391 onError: _emitError,
392 onDone: completer.complete,
393 cancelOnError: true);
394 return completer.future;
395 }
396
397 /// Emit an event with the given [type] and [path].
398 void _emitEvent(ChangeType type, String path) {
399 if (!isReady) return;
400
401 _eventsController.add(new WatchEvent(type, path));
402 }
403
404 /// Emit an error, then close the watcher.
405 void _emitError(error, StackTrace stackTrace) {
406 _eventsController.addError(error, stackTrace);
407 close();
408 }
409 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698