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