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

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((_) {
94 if (MacOSDirectoryWatcher.logDebugInfo) {
95 print("[$_id] finished initial directory list");
96 }
97 }),
98 _waitForBogusEvents()
99 ]).then((_) {
94 if (MacOSDirectoryWatcher.logDebugInfo) { 100 if (MacOSDirectoryWatcher.logDebugInfo) {
95 print("[$_id] watcher is ready, known files:"); 101 print("[$_id] watcher is ready, known files:");
96 for (var file in _files.toSet()) { 102 for (var file in _files.toSet()) {
97 print("[$_id] ${p.relative(file, from: directory)}"); 103 print("[$_id] ${p.relative(file, from: directory)}");
98 } 104 }
99 } 105 }
100 _readyCompleter.complete(); 106 _readyCompleter.complete();
101 }, 107 });
102 cancelOnError: true);
103 } 108 }
104 109
105 void close() { 110 void close() {
106 if (MacOSDirectoryWatcher.logDebugInfo) { 111 if (MacOSDirectoryWatcher.logDebugInfo) {
107 print("[$_id] watcher is closed"); 112 print("[$_id] watcher is closed");
108 } 113 }
109 for (var subscription in _subscriptions) {
110 subscription.cancel();
111 }
112 _subscriptions.clear();
113 if (_watchSubscription != null) _watchSubscription.cancel(); 114 if (_watchSubscription != null) _watchSubscription.cancel();
115 if (_initialListSubscription != null) _initialListSubscription.cancel();
116 if (_listSubscription != null) _listSubscription.cancel();
114 _watchSubscription = null; 117 _watchSubscription = null;
118 _initialListSubscription = null;
119 _listSubscription = null;
115 _eventsController.close(); 120 _eventsController.close();
116 } 121 }
117 122
118 /// The callback that's run when [Directory.watch] emits a batch of events. 123 /// The callback that's run when [Directory.watch] emits a batch of events.
119 void _onBatch(List<FileSystemEvent> batch) { 124 void _onBatch(List<FileSystemEvent> batch) {
120 if (MacOSDirectoryWatcher.logDebugInfo) { 125 if (MacOSDirectoryWatcher.logDebugInfo) {
121 print("[$_id] ======== batch:"); 126 print("[$_id] ======== batch:");
122 for (var event in batch) { 127 for (var event in batch) {
123 print("[$_id] ${_formatEvent(event)}"); 128 print("[$_id] ${_formatEvent(event)}");
124 } 129 }
125 130
126 print("[$_id] known files:"); 131 print("[$_id] known files:");
127 for (var file in _files.toSet()) { 132 for (var file in _files.toSet()) {
128 print("[$_id] ${p.relative(file, from: directory)}"); 133 print("[$_id] ${p.relative(file, from: directory)}");
129 } 134 }
130 } 135 }
131 136
132 _batches++; 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 }
133 159
134 _sortEvents(batch).forEach((path, events) { 160 _sortEvents(batch).forEach((path, events) {
135 var relativePath = p.relative(path, from: directory); 161 var relativePath = p.relative(path, from: directory);
136 if (MacOSDirectoryWatcher.logDebugInfo) { 162 if (MacOSDirectoryWatcher.logDebugInfo) {
137 print("[$_id] events for $relativePath:"); 163 print("[$_id] events for $relativePath:");
138 for (var event in events) { 164 for (var event in events) {
139 print("[$_id] ${_formatEvent(event)}"); 165 print("[$_id] ${_formatEvent(event)}");
140 } 166 }
141 } 167 }
142 168
(...skipping 15 matching lines...) Expand all
158 // that happened prior to the watch beginning. 184 // that happened prior to the watch beginning.
159 if (_files.contains(path)) continue; 185 if (_files.contains(path)) continue;
160 186
161 _emitEvent(ChangeType.ADD, path); 187 _emitEvent(ChangeType.ADD, path);
162 _files.add(path); 188 _files.add(path);
163 continue; 189 continue;
164 } 190 }
165 191
166 if (_files.containsDir(path)) continue; 192 if (_files.containsDir(path)) continue;
167 193
168 _listen(Chain.track(new Directory(path).list(recursive: true)), 194 var stream = Chain.track(new Directory(path).list(recursive: true));
169 (entity) { 195 _listSubscription = stream.listen((entity) {
170 if (entity is Directory) return; 196 if (entity is Directory) return;
171 if (_files.contains(path)) return; 197 if (_files.contains(path)) return;
172 198
173 _emitEvent(ChangeType.ADD, entity.path); 199 _emitEvent(ChangeType.ADD, entity.path);
174 _files.add(entity.path); 200 _files.add(entity.path);
175 }, onError: (e, stackTrace) { 201 }, onError: (e, stackTrace) {
176 if (MacOSDirectoryWatcher.logDebugInfo) { 202 if (MacOSDirectoryWatcher.logDebugInfo) {
177 print("[$_id] got error listing $relativePath: $e"); 203 print("[$_id] got error listing $relativePath: $e");
178 } 204 }
179 _emitError(e, stackTrace); 205 _emitError(e, stackTrace);
(...skipping 179 matching lines...) Expand 10 before | Expand all | Expand 10 after
359 385
360 return events; 386 return events;
361 } 387 }
362 388
363 /// The callback that's run when the [Directory.watch] stream is closed. 389 /// The callback that's run when the [Directory.watch] stream is closed.
364 void _onDone() { 390 void _onDone() {
365 if (MacOSDirectoryWatcher.logDebugInfo) print("[$_id] stream closed"); 391 if (MacOSDirectoryWatcher.logDebugInfo) print("[$_id] stream closed");
366 392
367 _watchSubscription = null; 393 _watchSubscription = null;
368 394
369 // If the directory still exists and we haven't seen more than one batch, 395 // 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 396 // this is probably issue 14849 rather than a real close event. We should
371 // just restart the watcher. 397 // just restart the watcher.
372 if (_batches < 2 && new Directory(directory).existsSync()) { 398 if (!isReady && new Directory(directory).existsSync()) {
373 if (MacOSDirectoryWatcher.logDebugInfo) { 399 if (MacOSDirectoryWatcher.logDebugInfo) {
374 print("[$_id] fake closure (issue 14849), re-opening stream"); 400 print("[$_id] fake closure (issue 14849), re-opening stream");
375 } 401 }
376 _startWatch(); 402 _startWatch();
377 return; 403 return;
378 } 404 }
379 405
380 // FSEvents can fail to report the contents of the directory being removed 406 // 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 407 // when the directory itself is removed, so we need to manually mark the
382 // files as removed. 408 // files as removed.
383 for (var file in _files.toSet()) { 409 for (var file in _files.toSet()) {
384 _emitEvent(ChangeType.REMOVE, file); 410 _emitEvent(ChangeType.REMOVE, file);
385 } 411 }
386 _files.clear(); 412 _files.clear();
387 close(); 413 close();
388 } 414 }
389 415
390 /// Start or restart the underlying [Directory.watch] stream. 416 /// Start or restart the underlying [Directory.watch] stream.
391 void _startWatch() { 417 void _startWatch() {
392 // Batch the FSEvent changes together so that we can dedup events. 418 // Batch the FSEvent changes together so that we can dedup events.
393 var innerStream = 419 var innerStream =
394 Chain.track(new Directory(directory).watch(recursive: true)) 420 Chain.track(new Directory(directory).watch(recursive: true))
395 .transform(new BatchedStreamTransformer<FileSystemEvent>()); 421 .transform(new BatchedStreamTransformer<FileSystemEvent>());
396 _watchSubscription = innerStream.listen(_onBatch, 422 _watchSubscription = innerStream.listen(_onBatch,
397 onError: _eventsController.addError, 423 onError: _eventsController.addError,
398 onDone: _onDone); 424 onDone: _onDone);
399 } 425 }
400 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
401 /// Emit an event with the given [type] and [path]. 458 /// Emit an event with the given [type] and [path].
402 void _emitEvent(ChangeType type, String path) { 459 void _emitEvent(ChangeType type, String path) {
403 if (!isReady) return; 460 if (!isReady) return;
404 461
405 if (MacOSDirectoryWatcher.logDebugInfo) { 462 if (MacOSDirectoryWatcher.logDebugInfo) {
406 print("[$_id] emitting $type ${p.relative(path, from: directory)}"); 463 print("[$_id] emitting $type ${p.relative(path, from: directory)}");
407 } 464 }
408 465
409 _eventsController.add(new WatchEvent(type, path)); 466 _eventsController.add(new WatchEvent(type, path));
410 } 467 }
411 468
412 /// Emit an error, then close the watcher. 469 /// Emit an error, then close the watcher.
413 void _emitError(error, StackTrace stackTrace) { 470 void _emitError(error, StackTrace stackTrace) {
414 _eventsController.addError(error, stackTrace); 471 _eventsController.addError(error, stackTrace);
415 close(); 472 close();
416 } 473 }
417 474
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. 475 // TODO(nweiz): remove this when issue 15042 is fixed.
431 /// Return a human-friendly string representation of [event]. 476 /// Return a human-friendly string representation of [event].
432 String _formatEvent(FileSystemEvent event) { 477 String _formatEvent(FileSystemEvent event) {
433 if (event == null) return 'null'; 478 if (event == null) return 'null';
434 479
435 var path = p.relative(event.path, from: directory); 480 var path = p.relative(event.path, from: directory);
436 var type = event.isDirectory ? 'directory' : 'file'; 481 var type = event.isDirectory ? 'directory' : 'file';
437 if (event is FileSystemCreateEvent) { 482 if (event is FileSystemCreateEvent) {
438 return "create $type $path"; 483 return "create $type $path";
439 } else if (event is FileSystemDeleteEvent) { 484 } else if (event is FileSystemDeleteEvent) {
440 return "delete $type $path"; 485 return "delete $type $path";
441 } else if (event is FileSystemModifyEvent) { 486 } else if (event is FileSystemModifyEvent) {
442 return "modify $type $path"; 487 return "modify $type $path";
443 } else if (event is FileSystemMoveEvent) { 488 } else if (event is FileSystemMoveEvent) {
444 return "move $type $path to " 489 return "move $type $path to "
445 "${p.relative(event.destination, from: directory)}"; 490 "${p.relative(event.destination, from: directory)}";
446 } 491 }
447 } 492 }
448 } 493 }
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