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

Side by Side Diff: pkg/watcher/lib/src/directory_watcher/mac_os.dart

Issue 140013002: Take a broader approach to filtering out bogus Mac OS watcher events. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 6 years, 11 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 | Annotate | Revision Log
« no previous file with comments | « pkg/pkg.status ('k') | pkg/watcher/test/utils.dart » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file 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 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. 3 // BSD-style license that can be found in the LICENSE file.
4 4
5 library watcher.directory_watcher.mac_os; 5 library watcher.directory_watcher.mac_os;
6 6
7 import 'dart:async'; 7 import 'dart:async';
8 import 'dart:io'; 8 import 'dart:io';
9 9
10 import 'package:path/path.dart' as p; 10 import 'package:path/path.dart' as p;
(...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after
43 final String directory; 43 final String directory;
44 44
45 Stream<WatchEvent> get events => _eventsController.stream; 45 Stream<WatchEvent> get events => _eventsController.stream;
46 final _eventsController = new StreamController<WatchEvent>.broadcast(); 46 final _eventsController = new StreamController<WatchEvent>.broadcast();
47 47
48 bool get isReady => _readyCompleter.isCompleted; 48 bool get isReady => _readyCompleter.isCompleted;
49 49
50 Future get ready => _readyCompleter.future; 50 Future get ready => _readyCompleter.future;
51 final _readyCompleter = new Completer(); 51 final _readyCompleter = new Completer();
52 52
53 /// The number of event batches that have been received from
54 /// [Directory.watch].
55 ///
56 /// This is used to determine if the [Directory.watch] stream was falsely
57 /// closed due to issue 14849. A close caused by events in the past will only
58 /// happen before or immediately after the first batch of events.
59 int _batches = 0;
60
61 /// The set of files that are known to exist recursively within the watched 53 /// The set of files that are known to exist recursively within the watched
62 /// directory. 54 /// directory.
63 /// 55 ///
64 /// The state of files on the filesystem is compared against this to determine 56 /// The state of files on the filesystem is compared against this to determine
65 /// the real change that occurred when working around issue 14373. This is 57 /// the real change that occurred when working around issue 14373. This is
66 /// also used to emit REMOVE events when subdirectories are moved out of the 58 /// also used to emit REMOVE events when subdirectories are moved out of the
67 /// watched directory. 59 /// watched directory.
68 final PathSet _files; 60 final PathSet _files;
69 61
70 /// The subscription to the stream returned by [Directory.watch]. 62 /// The subscription to the stream returned by [Directory.watch].
71 /// 63 ///
72 /// This is separate from [_subscriptions] because this stream occasionally 64 /// This is separate from [_subscriptions] because this stream occasionally
73 /// needs to be resubscribed in order to work around issue 14849. 65 /// needs to be resubscribed in order to work around issue 14849.
74 StreamSubscription<FileSystemEvent> _watchSubscription; 66 StreamSubscription<FileSystemEvent> _watchSubscription;
75 67
76 /// A set of subscriptions that this watcher subscribes to. 68 /// The subscription to the [Directory.list] call for the initial listing of
77 /// 69 /// the directory to determine its initial state.
78 /// These are gathered together so that they may all be canceled when the 70 StreamSubscription<FileSystemEntity> _initialListSubscription;
79 /// watcher is closed. This does not include [_watchSubscription]. 71
80 final _subscriptions = new Set<StreamSubscription>(); 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;
81 79
82 _MacOSDirectoryWatcher(String directory, int parentId) 80 _MacOSDirectoryWatcher(String directory, int parentId)
83 : directory = directory, 81 : directory = directory,
84 _files = new PathSet(directory), 82 _files = new PathSet(directory),
85 _id = "$parentId/${_count++}" { 83 _id = "$parentId/${_count++}" {
86 _startWatch(); 84 _startWatch();
87 85
88 _listen(Chain.track(new Directory(directory).list(recursive: true)), 86 // Before we're ready to emit events, wait for [_listDir] to complete and
89 (entity) { 87 // for enough time to elapse that if bogus events (issue 14373) would be
90 if (entity is! Directory) _files.add(entity.path); 88 // emitted, they will be.
91 }, 89 //
92 onError: _emitError, 90 // If we do receive a batch of events, [_onBatch] will ensure that these
93 onDone: () { 91 // futures don't fire and that the directory is re-listed.
92 Future.wait([
93 _listDir().then((_) => print("[$_id] finished initial directory list")),
Bob Nystrom 2014/01/15 21:32:31 Wrap this print in a debug check.
nweiz 2014/01/15 21:49:17 Done.
94 _waitForBogusEvents()
95 ]).then((_) {
94 if (MacOSDirectoryWatcher.logDebugInfo) { 96 if (MacOSDirectoryWatcher.logDebugInfo) {
95 print("[$_id] watcher is ready, known files:"); 97 print("[$_id] watcher is ready, known files:");
96 for (var file in _files.toSet()) { 98 for (var file in _files.toSet()) {
97 print("[$_id] ${p.relative(file, from: directory)}"); 99 print("[$_id] ${p.relative(file, from: directory)}");
98 } 100 }
99 } 101 }
100 _readyCompleter.complete(); 102 _readyCompleter.complete();
101 }, 103 });
102 cancelOnError: true);
103 } 104 }
104 105
105 void close() { 106 void close() {
106 if (MacOSDirectoryWatcher.logDebugInfo) { 107 if (MacOSDirectoryWatcher.logDebugInfo) {
107 print("[$_id] watcher is closed"); 108 print("[$_id] watcher is closed");
108 } 109 }
109 for (var subscription in _subscriptions) {
110 subscription.cancel();
111 }
112 _subscriptions.clear();
113 if (_watchSubscription != null) _watchSubscription.cancel(); 110 if (_watchSubscription != null) _watchSubscription.cancel();
111 if (_initialListSubscription != null) _initialListSubscription.cancel();
112 if (_listSubscription != null) _listSubscription.cancel();
114 _watchSubscription = null; 113 _watchSubscription = null;
114 _initialListSubscription = null;
115 _listSubscription = null;
115 _eventsController.close(); 116 _eventsController.close();
116 } 117 }
117 118
118 /// The callback that's run when [Directory.watch] emits a batch of events. 119 /// The callback that's run when [Directory.watch] emits a batch of events.
119 void _onBatch(List<FileSystemEvent> batch) { 120 void _onBatch(List<FileSystemEvent> batch) {
120 if (MacOSDirectoryWatcher.logDebugInfo) { 121 if (MacOSDirectoryWatcher.logDebugInfo) {
121 print("[$_id] ======== batch:"); 122 print("[$_id] ======== batch:");
122 for (var event in batch) { 123 for (var event in batch) {
123 print("[$_id] ${_formatEvent(event)}"); 124 print("[$_id] ${_formatEvent(event)}");
124 } 125 }
125 126
126 print("[$_id] known files:"); 127 print("[$_id] known files:");
127 for (var file in _files.toSet()) { 128 for (var file in _files.toSet()) {
128 print("[$_id] ${p.relative(file, from: directory)}"); 129 print("[$_id] ${p.relative(file, from: directory)}");
129 } 130 }
130 } 131 }
131 132
132 _batches++; 133 // If we get a batch of events before we're ready to begin emitting events,
134 // it's probable that it's a batch of pre-watcher events (see issue 14373).
135 // Ignore those events and re-list the directory.
136 if (!isReady) {
137 if (MacOSDirectoryWatcher.logDebugInfo) {
138 print("[$_id] not ready to emit events, re-listing directory");
139 }
140
141 // Cancel the timer because this batch is the only batch of bogus events
Bob Nystrom 2014/01/15 21:32:31 Reword more like "because bogus events only appear
nweiz 2014/01/15 21:49:17 Done.
142 // we'll receive.
143 _bogusEventTimer.cancel();
144 _listDir().then((_) {
145 if (MacOSDirectoryWatcher.logDebugInfo) {
146 print("[$_id] watcher is ready, known files:");
147 for (var file in _files.toSet()) {
148 print("[$_id] ${p.relative(file, from: directory)}");
149 }
150 }
151 _readyCompleter.complete();
152 });
153 return;
154 }
133 155
134 _sortEvents(batch).forEach((path, events) { 156 _sortEvents(batch).forEach((path, events) {
135 var relativePath = p.relative(path, from: directory); 157 var relativePath = p.relative(path, from: directory);
136 if (MacOSDirectoryWatcher.logDebugInfo) { 158 if (MacOSDirectoryWatcher.logDebugInfo) {
137 print("[$_id] events for $relativePath:"); 159 print("[$_id] events for $relativePath:");
138 for (var event in events) { 160 for (var event in events) {
139 print("[$_id] ${_formatEvent(event)}"); 161 print("[$_id] ${_formatEvent(event)}");
140 } 162 }
141 } 163 }
142 164
(...skipping 15 matching lines...) Expand all
158 // that happened prior to the watch beginning. 180 // that happened prior to the watch beginning.
159 if (_files.contains(path)) continue; 181 if (_files.contains(path)) continue;
160 182
161 _emitEvent(ChangeType.ADD, path); 183 _emitEvent(ChangeType.ADD, path);
162 _files.add(path); 184 _files.add(path);
163 continue; 185 continue;
164 } 186 }
165 187
166 if (_files.containsDir(path)) continue; 188 if (_files.containsDir(path)) continue;
167 189
168 _listen(Chain.track(new Directory(path).list(recursive: true)), 190 var stream = Chain.track(new Directory(path).list(recursive: true));
169 (entity) { 191 _listSubscription = stream.listen((entity) {
170 if (entity is Directory) return; 192 if (entity is Directory) return;
171 if (_files.contains(path)) return; 193 if (_files.contains(path)) return;
172 194
173 _emitEvent(ChangeType.ADD, entity.path); 195 _emitEvent(ChangeType.ADD, entity.path);
174 _files.add(entity.path); 196 _files.add(entity.path);
175 }, onError: (e, stackTrace) { 197 }, onError: (e, stackTrace) {
176 if (MacOSDirectoryWatcher.logDebugInfo) { 198 if (MacOSDirectoryWatcher.logDebugInfo) {
177 print("[$_id] got error listing $relativePath: $e"); 199 print("[$_id] got error listing $relativePath: $e");
178 } 200 }
179 _emitError(e, stackTrace); 201 _emitError(e, stackTrace);
(...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after
359 381
360 return events; 382 return events;
361 } 383 }
362 384
363 /// The callback that's run when the [Directory.watch] stream is closed. 385 /// The callback that's run when the [Directory.watch] stream is closed.
364 void _onDone() { 386 void _onDone() {
365 if (MacOSDirectoryWatcher.logDebugInfo) print("[$_id] stream closed"); 387 if (MacOSDirectoryWatcher.logDebugInfo) print("[$_id] stream closed");
366 388
367 _watchSubscription = null; 389 _watchSubscription = null;
368 390
369 // If the directory still exists and we haven't seen more than one batch, 391 // If the directory still exists and we're still expecting bogus events,
370 // this is probably issue 14849 rather than a real close event. We should 392 // this is probably issue 14849 rather than a real close event. We should
371 // just restart the watcher. 393 // just restart the watcher.
372 if (_batches < 2 && new Directory(directory).existsSync()) { 394 if (!isReady && new Directory(directory).existsSync()) {
373 if (MacOSDirectoryWatcher.logDebugInfo) { 395 if (MacOSDirectoryWatcher.logDebugInfo) {
374 print("[$_id] fake closure (issue 14849), re-opening stream"); 396 print("[$_id] fake closure (issue 14849), re-opening stream");
375 } 397 }
376 _startWatch(); 398 _startWatch();
377 return; 399 return;
378 } 400 }
379 401
380 // FSEvents can fail to report the contents of the directory being removed 402 // FSEvents can fail to report the contents of the directory being removed
381 // when the directory itself is removed, so we need to manually mark the 403 // when the directory itself is removed, so we need to manually mark the
382 // files as removed. 404 // files as removed.
383 for (var file in _files.toSet()) { 405 for (var file in _files.toSet()) {
384 _emitEvent(ChangeType.REMOVE, file); 406 _emitEvent(ChangeType.REMOVE, file);
385 } 407 }
386 _files.clear(); 408 _files.clear();
387 close(); 409 close();
388 } 410 }
389 411
390 /// Start or restart the underlying [Directory.watch] stream. 412 /// Start or restart the underlying [Directory.watch] stream.
391 void _startWatch() { 413 void _startWatch() {
392 // Batch the FSEvent changes together so that we can dedup events. 414 // Batch the FSEvent changes together so that we can dedup events.
393 var innerStream = 415 var innerStream =
394 Chain.track(new Directory(directory).watch(recursive: true)) 416 Chain.track(new Directory(directory).watch(recursive: true))
395 .transform(new BatchedStreamTransformer<FileSystemEvent>()); 417 .transform(new BatchedStreamTransformer<FileSystemEvent>());
396 _watchSubscription = innerStream.listen(_onBatch, 418 _watchSubscription = innerStream.listen(_onBatch,
397 onError: _eventsController.addError, 419 onError: _eventsController.addError,
398 onDone: _onDone); 420 onDone: _onDone);
399 } 421 }
400 422
423 /// Starts or restarts listing the watched directory to get an initial picture
424 /// of its state.
425 Future _listDir() {
426 assert(!isReady);
427 if (_initialListSubscription != null) _initialListSubscription.cancel();
428
429 _files.clear();
430 var completer = new Completer();
431 var stream = Chain.track(new Directory(directory).list(recursive: true));
432 _initialListSubscription = stream.listen((entity) {
433 if (entity is! Directory) _files.add(entity.path);
434 },
435 onError: _emitError,
436 onDone: completer.complete,
437 cancelOnError: true);
438 return completer.future;
439 }
440
441 /// Wait 200ms for a batch of bogus events (issue 14373) to come in.
442 ///
443 /// 200ms is short in terms of human interaction, but longer than any Mac OS
444 /// watcher tests take on the bots, so it should be safe to assume that any
445 /// bogus events will be signaled in that time frame.
446 Future _waitForBogusEvents() {
447 var completer = new Completer();
448 _bogusEventTimer = new Timer(
449 new Duration(milliseconds: 200),
450 completer.complete);
451 return completer.future;
452 }
453
401 /// Emit an event with the given [type] and [path]. 454 /// Emit an event with the given [type] and [path].
402 void _emitEvent(ChangeType type, String path) { 455 void _emitEvent(ChangeType type, String path) {
403 if (!isReady) return; 456 if (!isReady) return;
404 457
405 if (MacOSDirectoryWatcher.logDebugInfo) { 458 if (MacOSDirectoryWatcher.logDebugInfo) {
406 print("[$_id] emitting $type ${p.relative(path, from: directory)}"); 459 print("[$_id] emitting $type ${p.relative(path, from: directory)}");
407 } 460 }
408 461
409 _eventsController.add(new WatchEvent(type, path)); 462 _eventsController.add(new WatchEvent(type, path));
410 } 463 }
411 464
412 /// Emit an error, then close the watcher. 465 /// Emit an error, then close the watcher.
413 void _emitError(error, StackTrace stackTrace) { 466 void _emitError(error, StackTrace stackTrace) {
414 _eventsController.addError(error, stackTrace); 467 _eventsController.addError(error, stackTrace);
415 close(); 468 close();
416 } 469 }
417 470
418 /// Like [Stream.listen], but automatically adds the subscription to
419 /// [_subscriptions] so that it can be canceled when [close] is called.
420 void _listen(Stream stream, void onData(event), {Function onError,
421 void onDone(), bool cancelOnError}) {
422 var subscription;
423 subscription = stream.listen(onData, onError: onError, onDone: () {
424 _subscriptions.remove(subscription);
425 if (onDone != null) onDone();
426 }, cancelOnError: cancelOnError);
427 _subscriptions.add(subscription);
428 }
429
430 // TODO(nweiz): remove this when issue 15042 is fixed. 471 // TODO(nweiz): remove this when issue 15042 is fixed.
431 /// Return a human-friendly string representation of [event]. 472 /// Return a human-friendly string representation of [event].
432 String _formatEvent(FileSystemEvent event) { 473 String _formatEvent(FileSystemEvent event) {
433 if (event == null) return 'null'; 474 if (event == null) return 'null';
434 475
435 var path = p.relative(event.path, from: directory); 476 var path = p.relative(event.path, from: directory);
436 var type = event.isDirectory ? 'directory' : 'file'; 477 var type = event.isDirectory ? 'directory' : 'file';
437 if (event is FileSystemCreateEvent) { 478 if (event is FileSystemCreateEvent) {
438 return "create $type $path"; 479 return "create $type $path";
439 } else if (event is FileSystemDeleteEvent) { 480 } else if (event is FileSystemDeleteEvent) {
440 return "delete $type $path"; 481 return "delete $type $path";
441 } else if (event is FileSystemModifyEvent) { 482 } else if (event is FileSystemModifyEvent) {
442 return "modify $type $path"; 483 return "modify $type $path";
443 } else if (event is FileSystemMoveEvent) { 484 } else if (event is FileSystemMoveEvent) {
444 return "move $type $path to " 485 return "move $type $path to "
445 "${p.relative(event.destination, from: directory)}"; 486 "${p.relative(event.destination, from: directory)}";
446 } 487 }
447 } 488 }
448 } 489 }
OLDNEW
« no previous file with comments | « pkg/pkg.status ('k') | pkg/watcher/test/utils.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698