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

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

Issue 1400473008: Roll Observatory packages and add a roll script (Closed) Base URL: git@github.com:dart-lang/observatory_pub_packages.git@master
Patch Set: Created 5 years, 2 months 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
« no previous file with comments | « watcher/lib/src/directory_watcher/polling.dart ('k') | watcher/lib/src/file_watcher.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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
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 }
OLDNEW
« no previous file with comments | « watcher/lib/src/directory_watcher/polling.dart ('k') | watcher/lib/src/file_watcher.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698