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

Side by Side Diff: observatory_pub_packages/watcher/src/directory_watcher/mac_os.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) 2013, 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
5 library watcher.directory_watcher.mac_os;
6
7 import 'dart:async';
8 import 'dart:io';
9
10 import 'package:path/path.dart' as p;
11 import 'package:stack_trace/stack_trace.dart';
12
13 import '../constructable_file_system_event.dart';
14 import '../path_set.dart';
15 import '../utils.dart';
16 import '../watch_event.dart';
17 import 'resubscribable.dart';
18
19 /// Uses the FSEvents subsystem to watch for filesystem events.
20 ///
21 /// FSEvents has two main idiosyncrasies that this class works around. First, it
22 /// will occasionally report events that occurred before the filesystem watch
23 /// was initiated. Second, if multiple events happen to the same file in close
24 /// succession, it won't report them in the order they occurred. See issue
25 /// 14373.
26 ///
27 /// This also works around issues 16003 and 14849 in the implementation of
28 /// [Directory.watch].
29 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher {
30 // TODO(nweiz): remove these when issue 15042 is fixed.
31 static var logDebugInfo = false;
32 static var _count = 0;
33
34 MacOSDirectoryWatcher(String directory)
35 : super(directory, () => new _MacOSDirectoryWatcher(directory, _count++));
36 }
37
38 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher {
39 // TODO(nweiz): remove these when issue 15042 is fixed.
40 static var _count = 0;
41 final String _id;
42
43 final String directory;
44
45 Stream<WatchEvent> get events => _eventsController.stream;
46 final _eventsController = new StreamController<WatchEvent>.broadcast();
47
48 bool get isReady => _readyCompleter.isCompleted;
49
50 Future get ready => _readyCompleter.future;
51 final _readyCompleter = new Completer();
52
53 /// The set of files that are known to exist recursively within the watched
54 /// directory.
55 ///
56 /// The state of files on the filesystem is compared against this to determine
57 /// the real change that occurred when working around issue 14373. This is
58 /// also used to emit REMOVE events when subdirectories are moved out of the
59 /// watched directory.
60 final PathSet _files;
61
62 /// The subscription to the stream returned by [Directory.watch].
63 ///
64 /// This is separate from [_subscriptions] because this stream occasionally
65 /// needs to be resubscribed in order to work around issue 14849.
66 StreamSubscription<FileSystemEvent> _watchSubscription;
67
68 /// The subscription to the [Directory.list] call for the initial listing of
69 /// the directory to determine its initial state.
70 StreamSubscription<FileSystemEntity> _initialListSubscription;
71
72 /// The subscription to the [Directory.list] call for listing the contents of
73 /// a subdirectory that was moved into the watched directory.
74 StreamSubscription<FileSystemEntity> _listSubscription;
75
76 /// The timer for tracking how long we wait for an initial batch of bogus
77 /// events (see issue 14373).
78 Timer _bogusEventTimer;
79
80 _MacOSDirectoryWatcher(String directory, int parentId)
81 : directory = directory,
82 _files = new PathSet(directory),
83 _id = "$parentId/${_count++}" {
84 _startWatch();
85
86 // Before we're ready to emit events, wait for [_listDir] to complete and
87 // for enough time to elapse that if bogus events (issue 14373) would be
88 // emitted, they will be.
89 //
90 // If we do receive a batch of events, [_onBatch] will ensure that these
91 // futures don't fire and that the directory is re-listed.
92 Future.wait([
93 _listDir().then((_) {
94 if (MacOSDirectoryWatcher.logDebugInfo) {
95 print("[$_id] finished initial directory list");
96 }
97 }),
98 _waitForBogusEvents()
99 ]).then((_) {
100 if (MacOSDirectoryWatcher.logDebugInfo) {
101 print("[$_id] watcher is ready, known files:");
102 for (var file in _files.toSet()) {
103 print("[$_id] ${p.relative(file, from: directory)}");
104 }
105 }
106 _readyCompleter.complete();
107 });
108 }
109
110 void close() {
111 if (MacOSDirectoryWatcher.logDebugInfo) {
112 print("[$_id] watcher is closed\n${new Chain.current().terse}");
113 }
114 if (_watchSubscription != null) _watchSubscription.cancel();
115 if (_initialListSubscription != null) _initialListSubscription.cancel();
116 if (_listSubscription != null) _listSubscription.cancel();
117 _watchSubscription = null;
118 _initialListSubscription = null;
119 _listSubscription = null;
120 _eventsController.close();
121 }
122
123 /// The callback that's run when [Directory.watch] emits a batch of events.
124 void _onBatch(List<FileSystemEvent> batch) {
125 if (MacOSDirectoryWatcher.logDebugInfo) {
126 print("[$_id] ======== batch:");
127 for (var event in batch) {
128 print("[$_id] ${_formatEvent(event)}");
129 }
130
131 print("[$_id] known files:");
132 for (var file in _files.toSet()) {
133 print("[$_id] ${p.relative(file, from: directory)}");
134 }
135 }
136
137 // If we get a batch of events before we're ready to begin emitting events,
138 // it's probable that it's a batch of pre-watcher events (see issue 14373).
139 // Ignore those events and re-list the directory.
140 if (!isReady) {
141 if (MacOSDirectoryWatcher.logDebugInfo) {
142 print("[$_id] not ready to emit events, re-listing directory");
143 }
144
145 // Cancel the timer because bogus events only occur in the first batch, so
146 // we can fire [ready] as soon as we're done listing the directory.
147 _bogusEventTimer.cancel();
148 _listDir().then((_) {
149 if (MacOSDirectoryWatcher.logDebugInfo) {
150 print("[$_id] watcher is ready, known files:");
151 for (var file in _files.toSet()) {
152 print("[$_id] ${p.relative(file, from: directory)}");
153 }
154 }
155 _readyCompleter.complete();
156 });
157 return;
158 }
159
160 _sortEvents(batch).forEach((path, events) {
161 var relativePath = p.relative(path, from: directory);
162 if (MacOSDirectoryWatcher.logDebugInfo) {
163 print("[$_id] events for $relativePath:");
164 for (var event in events) {
165 print("[$_id] ${_formatEvent(event)}");
166 }
167 }
168
169 var canonicalEvent = _canonicalEvent(events);
170 events = canonicalEvent == null ?
171 _eventsBasedOnFileSystem(path) : [canonicalEvent];
172 if (MacOSDirectoryWatcher.logDebugInfo) {
173 print("[$_id] canonical event for $relativePath: "
174 "${_formatEvent(canonicalEvent)}");
175 print("[$_id] actionable events for $relativePath: "
176 "${events.map(_formatEvent)}");
177 }
178
179 for (var event in events) {
180 if (event is FileSystemCreateEvent) {
181 if (!event.isDirectory) {
182 // Don't emit ADD events for files or directories that we already
183 // know about. Such an event comes from FSEvents reporting an add
184 // that happened prior to the watch beginning.
185 if (_files.contains(path)) continue;
186
187 _emitEvent(ChangeType.ADD, path);
188 _files.add(path);
189 continue;
190 }
191
192 if (_files.containsDir(path)) continue;
193
194 var stream = Chain.track(new Directory(path).list(recursive: true));
195 _listSubscription = stream.listen((entity) {
196 if (entity is Directory) return;
197 if (_files.contains(path)) return;
198
199 _emitEvent(ChangeType.ADD, entity.path);
200 _files.add(entity.path);
201 }, onError: (e, stackTrace) {
202 if (MacOSDirectoryWatcher.logDebugInfo) {
203 print("[$_id] got error listing $relativePath: $e");
204 }
205 _emitError(e, stackTrace);
206 }, cancelOnError: true);
207 } else if (event is FileSystemModifyEvent) {
208 assert(!event.isDirectory);
209 _emitEvent(ChangeType.MODIFY, path);
210 } else {
211 assert(event is FileSystemDeleteEvent);
212 for (var removedPath in _files.remove(path)) {
213 _emitEvent(ChangeType.REMOVE, removedPath);
214 }
215 }
216 }
217 });
218
219 if (MacOSDirectoryWatcher.logDebugInfo) {
220 print("[$_id] ======== batch complete");
221 }
222 }
223
224 /// Sort all the events in a batch into sets based on their path.
225 ///
226 /// A single input event may result in multiple events in the returned map;
227 /// for example, a MOVE event becomes a DELETE event for the source and a
228 /// CREATE event for the destination.
229 ///
230 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
231 /// contain any events relating to [directory].
232 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
233 var eventsForPaths = {};
234
235 // FSEvents can report past events, including events on the root directory
236 // such as it being created. We want to ignore these. If the directory is
237 // really deleted, that's handled by [_onDone].
238 batch = batch.where((event) => event.path != directory).toList();
239
240 // Events within directories that already have events are superfluous; the
241 // directory's full contents will be examined anyway, so we ignore such
242 // events. Emitting them could cause useless or out-of-order events.
243 var directories = unionAll(batch.map((event) {
244 if (!event.isDirectory) return new Set();
245 if (event is! FileSystemMoveEvent) return new Set.from([event.path]);
246 return new Set.from([event.path, event.destination]);
247 }));
248
249 isInModifiedDirectory(path) =>
250 directories.any((dir) => path != dir && path.startsWith(dir));
251
252 addEvent(path, event) {
253 if (isInModifiedDirectory(path)) return;
254 var set = eventsForPaths.putIfAbsent(path, () => new Set());
255 set.add(event);
256 }
257
258 for (var event in batch) {
259 // The Mac OS watcher doesn't emit move events. See issue 14806.
260 assert(event is! FileSystemMoveEvent);
261 addEvent(event.path, event);
262 }
263
264 return eventsForPaths;
265 }
266
267 /// Returns the canonical event from a batch of events on the same path, if
268 /// one exists.
269 ///
270 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
271 /// CREATE, or events with different values for [isDirectory]), this returns a
272 /// single event that describes what happened to the path in question.
273 ///
274 /// If [batch] does contain contradictory events, this returns `null` to
275 /// indicate that the state of the path on the filesystem should be checked to
276 /// determine what occurred.
277 FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) {
278 // An empty batch indicates that we've learned earlier that the batch is
279 // contradictory (e.g. because of a move).
280 if (batch.isEmpty) return null;
281
282 var type = batch.first.type;
283 var isDir = batch.first.isDirectory;
284 var hadModifyEvent = false;
285
286 for (var event in batch.skip(1)) {
287 // If one event reports that the file is a directory and another event
288 // doesn't, that's a contradiction.
289 if (isDir != event.isDirectory) return null;
290
291 // Modify events don't contradict either CREATE or REMOVE events. We can
292 // safely assume the file was modified after a CREATE or before the
293 // REMOVE; otherwise there will also be a REMOVE or CREATE event
294 // (respectively) that will be contradictory.
295 if (event is FileSystemModifyEvent) {
296 hadModifyEvent = true;
297 continue;
298 }
299 assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
300
301 // If we previously thought this was a MODIFY, we now consider it to be a
302 // CREATE or REMOVE event. This is safe for the same reason as above.
303 if (type == FileSystemEvent.MODIFY) {
304 type = event.type;
305 continue;
306 }
307
308 // A CREATE event contradicts a REMOVE event and vice versa.
309 assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE);
310 if (type != event.type) return null;
311 }
312
313 // If we got a CREATE event for a file we already knew about, that comes
314 // from FSEvents reporting an add that happened prior to the watch
315 // beginning. If we also received a MODIFY event, we want to report that,
316 // but not the CREATE.
317 if (type == FileSystemEvent.CREATE && hadModifyEvent &&
318 _files.contains(batch.first.path)) {
319 type = FileSystemEvent.MODIFY;
320 }
321
322 switch (type) {
323 case FileSystemEvent.CREATE:
324 // Issue 16003 means that a CREATE event for a directory can indicate
325 // that the directory was moved and then re-created.
326 // [_eventsBasedOnFileSystem] will handle this correctly by producing a
327 // DELETE event followed by a CREATE event if the directory exists.
328 if (isDir) return null;
329 return new ConstructableFileSystemCreateEvent(batch.first.path, false);
330 case FileSystemEvent.DELETE:
331 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir);
332 case FileSystemEvent.MODIFY:
333 return new ConstructableFileSystemModifyEvent(
334 batch.first.path, isDir, false);
335 default: assert(false);
336 }
337 }
338
339 /// Returns one or more events that describe the change between the last known
340 /// state of [path] and its current state on the filesystem.
341 ///
342 /// This returns a list whose order should be reflected in the events emitted
343 /// to the user, unlike the batched events from [Directory.watch]. The
344 /// returned list may be empty, indicating that no changes occurred to [path]
345 /// (probably indicating that it was created and then immediately deleted).
346 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
347 var fileExisted = _files.contains(path);
348 var dirExisted = _files.containsDir(path);
349 var fileExists = new File(path).existsSync();
350 var dirExists = new Directory(path).existsSync();
351
352 if (MacOSDirectoryWatcher.logDebugInfo) {
353 print("[$_id] checking file system for "
354 "${p.relative(path, from: directory)}");
355 print("[$_id] file existed: $fileExisted");
356 print("[$_id] dir existed: $dirExisted");
357 print("[$_id] file exists: $fileExists");
358 print("[$_id] dir exists: $dirExists");
359 }
360
361 var events = [];
362 if (fileExisted) {
363 if (fileExists) {
364 events.add(new ConstructableFileSystemModifyEvent(path, false, false));
365 } else {
366 events.add(new ConstructableFileSystemDeleteEvent(path, false));
367 }
368 } else if (dirExisted) {
369 if (dirExists) {
370 // If we got contradictory events for a directory that used to exist and
371 // still exists, we need to rescan the whole thing in case it was
372 // replaced with a different directory.
373 events.add(new ConstructableFileSystemDeleteEvent(path, true));
374 events.add(new ConstructableFileSystemCreateEvent(path, true));
375 } else {
376 events.add(new ConstructableFileSystemDeleteEvent(path, true));
377 }
378 }
379
380 if (!fileExisted && fileExists) {
381 events.add(new ConstructableFileSystemCreateEvent(path, false));
382 } else if (!dirExisted && dirExists) {
383 events.add(new ConstructableFileSystemCreateEvent(path, true));
384 }
385
386 return events;
387 }
388
389 /// The callback that's run when the [Directory.watch] stream is closed.
390 void _onDone() {
391 if (MacOSDirectoryWatcher.logDebugInfo) print("[$_id] stream closed");
392
393 _watchSubscription = null;
394
395 // If the directory still exists and we're still expecting bogus events,
396 // this is probably issue 14849 rather than a real close event. We should
397 // just restart the watcher.
398 if (!isReady && new Directory(directory).existsSync()) {
399 if (MacOSDirectoryWatcher.logDebugInfo) {
400 print("[$_id] fake closure (issue 14849), re-opening stream");
401 }
402 _startWatch();
403 return;
404 }
405
406 // FSEvents can fail to report the contents of the directory being removed
407 // when the directory itself is removed, so we need to manually mark the
408 // files as removed.
409 for (var file in _files.toSet()) {
410 _emitEvent(ChangeType.REMOVE, file);
411 }
412 _files.clear();
413 close();
414 }
415
416 /// Start or restart the underlying [Directory.watch] stream.
417 void _startWatch() {
418 // Batch the FSEvent changes together so that we can dedup events.
419 var innerStream =
420 Chain.track(new Directory(directory).watch(recursive: true))
421 .transform(new BatchedStreamTransformer<FileSystemEvent>());
422 _watchSubscription = innerStream.listen(_onBatch,
423 onError: _eventsController.addError,
424 onDone: _onDone);
425 }
426
427 /// Starts or restarts listing the watched directory to get an initial picture
428 /// of its state.
429 Future _listDir() {
430 assert(!isReady);
431 if (_initialListSubscription != null) _initialListSubscription.cancel();
432
433 _files.clear();
434 var completer = new Completer();
435 var stream = Chain.track(new Directory(directory).list(recursive: true));
436 _initialListSubscription = stream.listen((entity) {
437 if (entity is! Directory) _files.add(entity.path);
438 },
439 onError: _emitError,
440 onDone: completer.complete,
441 cancelOnError: true);
442 return completer.future;
443 }
444
445 /// Wait 200ms for a batch of bogus events (issue 14373) to come in.
446 ///
447 /// 200ms is short in terms of human interaction, but longer than any Mac OS
448 /// watcher tests take on the bots, so it should be safe to assume that any
449 /// bogus events will be signaled in that time frame.
450 Future _waitForBogusEvents() {
451 var completer = new Completer();
452 _bogusEventTimer = new Timer(
453 new Duration(milliseconds: 200),
454 completer.complete);
455 return completer.future;
456 }
457
458 /// Emit an event with the given [type] and [path].
459 void _emitEvent(ChangeType type, String path) {
460 if (!isReady) return;
461
462 if (MacOSDirectoryWatcher.logDebugInfo) {
463 print("[$_id] emitting $type ${p.relative(path, from: directory)}");
464 }
465
466 _eventsController.add(new WatchEvent(type, path));
467 }
468
469 /// Emit an error, then close the watcher.
470 void _emitError(error, StackTrace stackTrace) {
471 if (MacOSDirectoryWatcher.logDebugInfo) {
472 print("[$_id] emitting error: $error\n" +
473 "${new Chain.forTrace(stackTrace).terse}");
474 }
475 _eventsController.addError(error, stackTrace);
476 close();
477 }
478
479 // TODO(nweiz): remove this when issue 15042 is fixed.
480 /// Return a human-friendly string representation of [event].
481 String _formatEvent(FileSystemEvent event) {
482 if (event == null) return 'null';
483
484 var path = p.relative(event.path, from: directory);
485 var type = event.isDirectory ? 'directory' : 'file';
486 if (event is FileSystemCreateEvent) {
487 return "create $type $path";
488 } else if (event is FileSystemDeleteEvent) {
489 return "delete $type $path";
490 } else if (event is FileSystemModifyEvent) {
491 return "modify $type $path";
492 } else if (event is FileSystemMoveEvent) {
493 return "move $type $path to "
494 "${p.relative(event.destination, from: directory)}";
495 }
496 }
497 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698