Chromium Code Reviews| OLD | NEW |
|---|---|
| 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:stack_trace/stack_trace.dart'; | 11 import 'package:stack_trace/stack_trace.dart'; |
| 11 | 12 |
| 12 import '../constructable_file_system_event.dart'; | 13 import '../constructable_file_system_event.dart'; |
| 13 import '../path_set.dart'; | 14 import '../path_set.dart'; |
| 14 import '../utils.dart'; | 15 import '../utils.dart'; |
| 15 import '../watch_event.dart'; | 16 import '../watch_event.dart'; |
| 16 import 'resubscribable.dart'; | 17 import 'resubscribable.dart'; |
| 17 | 18 |
| 18 /// Uses the FSEvents subsystem to watch for filesystem events. | 19 /// Uses the FSEvents subsystem to watch for filesystem events. |
| 19 /// | 20 /// |
| 20 /// FSEvents has two main idiosyncrasies that this class works around. First, it | 21 /// FSEvents has two main idiosyncrasies that this class works around. First, it |
| 21 /// will occasionally report events that occurred before the filesystem watch | 22 /// will occasionally report events that occurred before the filesystem watch |
| 22 /// was initiated. Second, if multiple events happen to the same file in close | 23 /// was initiated. Second, if multiple events happen to the same file in close |
| 23 /// succession, it won't report them in the order they occurred. See issue | 24 /// succession, it won't report them in the order they occurred. See issue |
| 24 /// 14373. | 25 /// 14373. |
| 25 /// | 26 /// |
| 26 /// This also works around issues 15458 and 14849 in the implementation of | 27 /// This also works around issues 15458 and 14849 in the implementation of |
| 27 /// [Directory.watch]. | 28 /// [Directory.watch]. |
| 28 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher { | 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 final int _id; | |
| 34 | |
| 29 MacOSDirectoryWatcher(String directory) | 35 MacOSDirectoryWatcher(String directory) |
| 30 : super(directory, () => new _MacOSDirectoryWatcher(directory)); | 36 : _id = _count++, |
| 37 super(directory, () => new _MacOSDirectoryWatcher(directory, _count)); | |
|
Bob Nystrom
2013/12/05 22:43:55
How does ++ work in an initialization list? Should
nweiz
2013/12/05 22:48:47
Come to think of it, there's really no reason the
| |
| 31 } | 38 } |
| 32 | 39 |
| 33 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher { | 40 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher { |
| 41 // TODO(nweiz): remove these when issue 15042 is fixed. | |
| 42 static var _count = 0; | |
| 43 final String _id; | |
| 44 | |
| 34 final String directory; | 45 final String directory; |
| 35 | 46 |
| 36 Stream<WatchEvent> get events => _eventsController.stream; | 47 Stream<WatchEvent> get events => _eventsController.stream; |
| 37 final _eventsController = new StreamController<WatchEvent>.broadcast(); | 48 final _eventsController = new StreamController<WatchEvent>.broadcast(); |
| 38 | 49 |
| 39 bool get isReady => _readyCompleter.isCompleted; | 50 bool get isReady => _readyCompleter.isCompleted; |
| 40 | 51 |
| 41 Future get ready => _readyCompleter.future; | 52 Future get ready => _readyCompleter.future; |
| 42 final _readyCompleter = new Completer(); | 53 final _readyCompleter = new Completer(); |
| 43 | 54 |
| (...skipping 19 matching lines...) Expand all Loading... | |
| 63 /// This is separate from [_subscriptions] because this stream occasionally | 74 /// This is separate from [_subscriptions] because this stream occasionally |
| 64 /// needs to be resubscribed in order to work around issue 14849. | 75 /// needs to be resubscribed in order to work around issue 14849. |
| 65 StreamSubscription<FileSystemEvent> _watchSubscription; | 76 StreamSubscription<FileSystemEvent> _watchSubscription; |
| 66 | 77 |
| 67 /// A set of subscriptions that this watcher subscribes to. | 78 /// A set of subscriptions that this watcher subscribes to. |
| 68 /// | 79 /// |
| 69 /// These are gathered together so that they may all be canceled when the | 80 /// These are gathered together so that they may all be canceled when the |
| 70 /// watcher is closed. This does not include [_watchSubscription]. | 81 /// watcher is closed. This does not include [_watchSubscription]. |
| 71 final _subscriptions = new Set<StreamSubscription>(); | 82 final _subscriptions = new Set<StreamSubscription>(); |
| 72 | 83 |
| 73 _MacOSDirectoryWatcher(String directory) | 84 _MacOSDirectoryWatcher(String directory, int parentId) |
| 74 : directory = directory, | 85 : directory = directory, |
| 75 _files = new PathSet(directory) { | 86 _files = new PathSet(directory), |
| 87 _id = "$parentId/${_count++}" { | |
| 76 _startWatch(); | 88 _startWatch(); |
| 77 | 89 |
| 78 _listen(Chain.track(new Directory(directory).list(recursive: true)), | 90 _listen(Chain.track(new Directory(directory).list(recursive: true)), |
| 79 (entity) { | 91 (entity) { |
| 80 if (entity is! Directory) _files.add(entity.path); | 92 if (entity is! Directory) _files.add(entity.path); |
| 81 }, | 93 }, |
| 82 onError: _emitError, | 94 onError: _emitError, |
| 83 onDone: _readyCompleter.complete, | 95 onDone: () { |
| 96 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 97 print("[$_id] watcher is ready, known files:"); | |
| 98 for (var file in _files.toSet()) { | |
| 99 print("[$_id] ${p.relative(file, from: directory)}"); | |
| 100 } | |
| 101 } | |
| 102 _readyCompleter.complete(); | |
| 103 }, | |
| 84 cancelOnError: true); | 104 cancelOnError: true); |
| 85 } | 105 } |
| 86 | 106 |
| 87 void close() { | 107 void close() { |
| 108 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 109 print("[$_id] watcher is closed"); | |
| 110 } | |
| 88 for (var subscription in _subscriptions) { | 111 for (var subscription in _subscriptions) { |
| 89 subscription.cancel(); | 112 subscription.cancel(); |
| 90 } | 113 } |
| 91 _subscriptions.clear(); | 114 _subscriptions.clear(); |
| 92 if (_watchSubscription != null) _watchSubscription.cancel(); | 115 if (_watchSubscription != null) _watchSubscription.cancel(); |
| 93 _watchSubscription = null; | 116 _watchSubscription = null; |
| 94 _eventsController.close(); | 117 _eventsController.close(); |
| 95 } | 118 } |
| 96 | 119 |
| 97 /// The callback that's run when [Directory.watch] emits a batch of events. | 120 /// The callback that's run when [Directory.watch] emits a batch of events. |
| 98 void _onBatch(List<FileSystemEvent> batch) { | 121 void _onBatch(List<FileSystemEvent> batch) { |
| 122 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 123 print("[$_id] ======== batch:"); | |
| 124 for (var event in batch) { | |
| 125 print("[$_id] ${_formatEvent(event)}"); | |
| 126 } | |
| 127 | |
| 128 print("[$_id] known files:"); | |
| 129 for (var file in _files.toSet()) { | |
| 130 print("[$_id] ${p.relative(file, from: directory)}"); | |
| 131 } | |
| 132 } | |
| 133 | |
| 99 batches++; | 134 batches++; |
| 100 | 135 |
| 101 _sortEvents(batch).forEach((path, events) { | 136 _sortEvents(batch).forEach((path, events) { |
| 137 var relativePath = p.relative(path, from: directory); | |
| 138 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 139 print("[$_id] events for $relativePath:\n"); | |
| 140 for (var event in events) { | |
| 141 print("[$_id] ${_formatEvent(event)}"); | |
| 142 } | |
| 143 } | |
| 144 | |
| 102 var canonicalEvent = _canonicalEvent(events); | 145 var canonicalEvent = _canonicalEvent(events); |
| 103 events = canonicalEvent == null ? | 146 events = canonicalEvent == null ? |
| 104 _eventsBasedOnFileSystem(path) : [canonicalEvent]; | 147 _eventsBasedOnFileSystem(path) : [canonicalEvent]; |
| 148 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 149 print("[$_id] canonical event for $relativePath: " | |
| 150 "${_formatEvent(canonicalEvent)}"); | |
| 151 print("[$_id] actionable events for $relativePath: " | |
| 152 "${events.map(_formatEvent)}"); | |
| 153 } | |
| 105 | 154 |
| 106 for (var event in events) { | 155 for (var event in events) { |
| 107 if (event is FileSystemCreateEvent) { | 156 if (event is FileSystemCreateEvent) { |
| 108 if (!event.isDirectory) { | 157 if (!event.isDirectory) { |
| 109 // Don't emit ADD events for files or directories that we already | 158 // Don't emit ADD events for files or directories that we already |
| 110 // know about. Such an event comes from FSEvents reporting an add | 159 // know about. Such an event comes from FSEvents reporting an add |
| 111 // that happened prior to the watch beginning. | 160 // that happened prior to the watch beginning. |
| 112 if (_files.contains(path)) continue; | 161 if (_files.contains(path)) continue; |
| 113 | 162 |
| 114 _emitEvent(ChangeType.ADD, path); | 163 _emitEvent(ChangeType.ADD, path); |
| 115 _files.add(path); | 164 _files.add(path); |
| 116 continue; | 165 continue; |
| 117 } | 166 } |
| 118 | 167 |
| 119 if (_files.containsDir(path)) continue; | 168 if (_files.containsDir(path)) continue; |
| 120 | 169 |
| 121 _listen(Chain.track(new Directory(path).list(recursive: true)), | 170 _listen(Chain.track(new Directory(path).list(recursive: true)), |
| 122 (entity) { | 171 (entity) { |
| 123 if (entity is Directory) return; | 172 if (entity is Directory) return; |
| 124 _emitEvent(ChangeType.ADD, entity.path); | 173 _emitEvent(ChangeType.ADD, entity.path); |
| 125 _files.add(entity.path); | 174 _files.add(entity.path); |
| 126 }, onError: _emitError, cancelOnError: true); | 175 }, onError: (e, stackTrace) { |
| 176 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 177 print("[$_id] got error listing $relativePath: $e"); | |
| 178 } | |
| 179 _emitError(e, stackTrace); | |
| 180 }, cancelOnError: true); | |
| 127 } else if (event is FileSystemModifyEvent) { | 181 } else if (event is FileSystemModifyEvent) { |
| 128 assert(!event.isDirectory); | 182 assert(!event.isDirectory); |
| 129 _emitEvent(ChangeType.MODIFY, path); | 183 _emitEvent(ChangeType.MODIFY, path); |
| 130 } else { | 184 } else { |
| 131 assert(event is FileSystemDeleteEvent); | 185 assert(event is FileSystemDeleteEvent); |
| 132 for (var removedPath in _files.remove(path)) { | 186 for (var removedPath in _files.remove(path)) { |
| 133 _emitEvent(ChangeType.REMOVE, removedPath); | 187 _emitEvent(ChangeType.REMOVE, removedPath); |
| 134 } | 188 } |
| 135 } | 189 } |
| 136 } | 190 } |
| 137 }); | 191 }); |
| 192 | |
| 193 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 194 print("[$_id] ======== batch complete"); | |
| 195 } | |
| 138 } | 196 } |
| 139 | 197 |
| 140 /// Sort all the events in a batch into sets based on their path. | 198 /// Sort all the events in a batch into sets based on their path. |
| 141 /// | 199 /// |
| 142 /// A single input event may result in multiple events in the returned map; | 200 /// A single input event may result in multiple events in the returned map; |
| 143 /// for example, a MOVE event becomes a DELETE event for the source and a | 201 /// for example, a MOVE event becomes a DELETE event for the source and a |
| 144 /// CREATE event for the destination. | 202 /// CREATE event for the destination. |
| 145 /// | 203 /// |
| 146 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it | 204 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it |
| 147 /// contain any events relating to [directory]. | 205 /// contain any events relating to [directory]. |
| (...skipping 108 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 256 /// This returns a list whose order should be reflected in the events emitted | 314 /// This returns a list whose order should be reflected in the events emitted |
| 257 /// to the user, unlike the batched events from [Directory.watch]. The | 315 /// to the user, unlike the batched events from [Directory.watch]. The |
| 258 /// returned list may be empty, indicating that no changes occurred to [path] | 316 /// returned list may be empty, indicating that no changes occurred to [path] |
| 259 /// (probably indicating that it was created and then immediately deleted). | 317 /// (probably indicating that it was created and then immediately deleted). |
| 260 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) { | 318 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) { |
| 261 var fileExisted = _files.contains(path); | 319 var fileExisted = _files.contains(path); |
| 262 var dirExisted = _files.containsDir(path); | 320 var dirExisted = _files.containsDir(path); |
| 263 var fileExists = new File(path).existsSync(); | 321 var fileExists = new File(path).existsSync(); |
| 264 var dirExists = new Directory(path).existsSync(); | 322 var dirExists = new Directory(path).existsSync(); |
| 265 | 323 |
| 324 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 325 print("[$_id] file existed: $fileExisted"); | |
| 326 print("[$_id] dir existed: $dirExisted"); | |
| 327 print("[$_id] file exists: $fileExists"); | |
| 328 print("[$_id] dir exists: $dirExists"); | |
| 329 } | |
| 330 | |
| 266 var events = []; | 331 var events = []; |
| 267 if (fileExisted) { | 332 if (fileExisted) { |
| 268 if (fileExists) { | 333 if (fileExists) { |
| 269 events.add(new ConstructableFileSystemModifyEvent(path, false, false)); | 334 events.add(new ConstructableFileSystemModifyEvent(path, false, false)); |
| 270 } else { | 335 } else { |
| 271 events.add(new ConstructableFileSystemDeleteEvent(path, false)); | 336 events.add(new ConstructableFileSystemDeleteEvent(path, false)); |
| 272 } | 337 } |
| 273 } else if (dirExisted) { | 338 } else if (dirExisted) { |
| 274 if (dirExists) { | 339 if (dirExists) { |
| 275 // If we got contradictory events for a directory that used to exist and | 340 // If we got contradictory events for a directory that used to exist and |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 321 .transform(new BatchedStreamTransformer<FileSystemEvent>()); | 386 .transform(new BatchedStreamTransformer<FileSystemEvent>()); |
| 322 _watchSubscription = innerStream.listen(_onBatch, | 387 _watchSubscription = innerStream.listen(_onBatch, |
| 323 onError: _eventsController.addError, | 388 onError: _eventsController.addError, |
| 324 onDone: _onDone); | 389 onDone: _onDone); |
| 325 } | 390 } |
| 326 | 391 |
| 327 /// Emit an event with the given [type] and [path]. | 392 /// Emit an event with the given [type] and [path]. |
| 328 void _emitEvent(ChangeType type, String path) { | 393 void _emitEvent(ChangeType type, String path) { |
| 329 if (!isReady) return; | 394 if (!isReady) return; |
| 330 | 395 |
| 396 if (MacOSDirectoryWatcher.logDebugInfo) { | |
| 397 print("[$_id] emitting $type ${p.relative(path, from: directory)}"); | |
| 398 } | |
| 399 | |
| 331 _eventsController.add(new WatchEvent(type, path)); | 400 _eventsController.add(new WatchEvent(type, path)); |
| 332 } | 401 } |
| 333 | 402 |
| 334 /// Emit an error, then close the watcher. | 403 /// Emit an error, then close the watcher. |
| 335 void _emitError(error, StackTrace stackTrace) { | 404 void _emitError(error, StackTrace stackTrace) { |
| 336 _eventsController.addError(error, stackTrace); | 405 _eventsController.addError(error, stackTrace); |
| 337 close(); | 406 close(); |
| 338 } | 407 } |
| 339 | 408 |
| 340 /// Like [Stream.listen], but automatically adds the subscription to | 409 /// Like [Stream.listen], but automatically adds the subscription to |
| 341 /// [_subscriptions] so that it can be canceled when [close] is called. | 410 /// [_subscriptions] so that it can be canceled when [close] is called. |
| 342 void _listen(Stream stream, void onData(event), {Function onError, | 411 void _listen(Stream stream, void onData(event), {Function onError, |
| 343 void onDone(), bool cancelOnError}) { | 412 void onDone(), bool cancelOnError}) { |
| 344 var subscription; | 413 var subscription; |
| 345 subscription = stream.listen(onData, onError: onError, onDone: () { | 414 subscription = stream.listen(onData, onError: onError, onDone: () { |
| 346 _subscriptions.remove(subscription); | 415 _subscriptions.remove(subscription); |
| 347 if (onDone != null) onDone(); | 416 if (onDone != null) onDone(); |
| 348 }, cancelOnError: cancelOnError); | 417 }, cancelOnError: cancelOnError); |
| 349 _subscriptions.add(subscription); | 418 _subscriptions.add(subscription); |
| 350 } | 419 } |
| 420 | |
| 421 // TODO(nweiz): remove this when issue 15042 is fixed. | |
| 422 /// Return a human-friendly string representation of [event]. | |
| 423 String _formatEvent(FileSystemEvent event) { | |
| 424 if (event == null) return 'null'; | |
| 425 | |
| 426 var path = p.relative(event.path, from: directory); | |
| 427 var type = event.isDirectory ? 'directory' : 'file'; | |
| 428 if (event is FileSystemCreateEvent) { | |
| 429 return "create $type $path"; | |
| 430 } else if (event is FileSystemDeleteEvent) { | |
| 431 return "delete $type $path"; | |
| 432 } else if (event is FileSystemModifyEvent) { | |
| 433 return "modify $type $path"; | |
| 434 } else if (event is FileSystemMoveEvent) { | |
| 435 return "move $type $path to " | |
| 436 "${p.relative(event.destination, from: directory)}"; | |
| 437 } | |
| 438 } | |
| 351 } | 439 } |
| OLD | NEW |