| 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 '../constructable_file_system_event.dart'; | 10 import '../constructable_file_system_event.dart'; |
| 11 import '../path_set.dart'; | 11 import '../path_set.dart'; |
| 12 import '../utils.dart'; | 12 import '../utils.dart'; |
| 13 import '../watch_event.dart'; | 13 import '../watch_event.dart'; |
| 14 import 'resubscribable.dart'; | 14 import 'resubscribable.dart'; |
| 15 | 15 |
| 16 /// Uses the FSEvents subsystem to watch for filesystem events. | 16 /// Uses the FSEvents subsystem to watch for filesystem events. |
| 17 /// | 17 /// |
| 18 /// FSEvents has two main idiosyncrasies that this class works around. First, it | 18 /// FSEvents has two main idiosyncrasies that this class works around. First, it |
| 19 /// will occasionally report events that occurred before the filesystem watch | 19 /// will occasionally report events that occurred before the filesystem watch |
| 20 /// was initiated. Second, if multiple events happen to the same file in close | 20 /// was initiated. Second, if multiple events happen to the same file in close |
| 21 /// succession, it won't report them in the order they occurred. See issue | 21 /// succession, it won't report them in the order they occurred. See issue |
| 22 /// 14373. | 22 /// 14373. |
| 23 /// | 23 /// |
| 24 /// This also works around issues 14793, 14806, and 14849 in the implementation | 24 /// This also works around issues 15458 and 14849 in the implementation of |
| 25 /// of [Directory.watch]. | 25 /// [Directory.watch]. |
| 26 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher { | 26 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher { |
| 27 MacOSDirectoryWatcher(String directory) | 27 MacOSDirectoryWatcher(String directory) |
| 28 : super(directory, () => new _MacOSDirectoryWatcher(directory)); | 28 : super(directory, () => new _MacOSDirectoryWatcher(directory)); |
| 29 } | 29 } |
| 30 | 30 |
| 31 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher { | 31 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher { |
| 32 final String directory; | 32 final String directory; |
| 33 | 33 |
| 34 Stream<WatchEvent> get events => _eventsController.stream; | 34 Stream<WatchEvent> get events => _eventsController.stream; |
| 35 final _eventsController = new StreamController<WatchEvent>.broadcast(); | 35 final _eventsController = new StreamController<WatchEvent>.broadcast(); |
| (...skipping 61 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 97 batches++; | 97 batches++; |
| 98 | 98 |
| 99 _sortEvents(batch).forEach((path, events) { | 99 _sortEvents(batch).forEach((path, events) { |
| 100 var canonicalEvent = _canonicalEvent(events); | 100 var canonicalEvent = _canonicalEvent(events); |
| 101 events = canonicalEvent == null ? | 101 events = canonicalEvent == null ? |
| 102 _eventsBasedOnFileSystem(path) : [canonicalEvent]; | 102 _eventsBasedOnFileSystem(path) : [canonicalEvent]; |
| 103 | 103 |
| 104 for (var event in events) { | 104 for (var event in events) { |
| 105 if (event is FileSystemCreateEvent) { | 105 if (event is FileSystemCreateEvent) { |
| 106 if (!event.isDirectory) { | 106 if (!event.isDirectory) { |
| 107 // Don't emit ADD events for files or directories that we already |
| 108 // know about. Such an event comes from FSEvents reporting an add |
| 109 // that happened prior to the watch beginning. |
| 110 if (_files.contains(path)) continue; |
| 111 |
| 107 _emitEvent(ChangeType.ADD, path); | 112 _emitEvent(ChangeType.ADD, path); |
| 108 _files.add(path); | 113 _files.add(path); |
| 109 continue; | 114 continue; |
| 110 } | 115 } |
| 111 | 116 |
| 117 if (_files.containsDir(path)) continue; |
| 118 |
| 112 _listen(new Directory(path).list(recursive: true), (entity) { | 119 _listen(new Directory(path).list(recursive: true), (entity) { |
| 113 if (entity is Directory) return; | 120 if (entity is Directory) return; |
| 114 _emitEvent(ChangeType.ADD, entity.path); | 121 _emitEvent(ChangeType.ADD, entity.path); |
| 115 _files.add(entity.path); | 122 _files.add(entity.path); |
| 116 }, onError: _emitError, cancelOnError: true); | 123 }, onError: _emitError, cancelOnError: true); |
| 117 } else if (event is FileSystemModifyEvent) { | 124 } else if (event is FileSystemModifyEvent) { |
| 118 assert(!event.isDirectory); | 125 assert(!event.isDirectory); |
| 119 _emitEvent(ChangeType.MODIFY, path); | 126 _emitEvent(ChangeType.MODIFY, path); |
| 120 } else { | 127 } else { |
| 121 assert(event is FileSystemDeleteEvent); | 128 assert(event is FileSystemDeleteEvent); |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 154 | 161 |
| 155 isInModifiedDirectory(path) => | 162 isInModifiedDirectory(path) => |
| 156 directories.any((dir) => path != dir && path.startsWith(dir)); | 163 directories.any((dir) => path != dir && path.startsWith(dir)); |
| 157 | 164 |
| 158 addEvent(path, event) { | 165 addEvent(path, event) { |
| 159 if (isInModifiedDirectory(path)) return; | 166 if (isInModifiedDirectory(path)) return; |
| 160 var set = eventsForPaths.putIfAbsent(path, () => new Set()); | 167 var set = eventsForPaths.putIfAbsent(path, () => new Set()); |
| 161 set.add(event); | 168 set.add(event); |
| 162 } | 169 } |
| 163 | 170 |
| 164 for (var event in batch.where((event) => event is! FileSystemMoveEvent)) { | 171 for (var event in batch) { |
| 172 // The Mac OS watcher doesn't emit move events. See issue 14806. |
| 173 assert(event is! FileSystemMoveEvent); |
| 165 addEvent(event.path, event); | 174 addEvent(event.path, event); |
| 166 } | 175 } |
| 167 | 176 |
| 168 // Issue 14806 means that move events can be misleading if they're in the | |
| 169 // same batch as another modification of a related file. If they are, we | |
| 170 // make the event set empty to ensure we check the state of the filesystem. | |
| 171 // Otherwise, treat them as a DELETE followed by an ADD. | |
| 172 for (var event in batch.where((event) => event is FileSystemMoveEvent)) { | |
| 173 if (eventsForPaths.containsKey(event.path) || | |
| 174 eventsForPaths.containsKey(event.destination)) { | |
| 175 | |
| 176 if (!isInModifiedDirectory(event.path)) { | |
| 177 eventsForPaths[event.path] = new Set(); | |
| 178 } | |
| 179 if (!isInModifiedDirectory(event.destination)) { | |
| 180 eventsForPaths[event.destination] = new Set(); | |
| 181 } | |
| 182 | |
| 183 continue; | |
| 184 } | |
| 185 | |
| 186 addEvent(event.path, new ConstructableFileSystemDeleteEvent( | |
| 187 event.path, event.isDirectory)); | |
| 188 addEvent(event.destination, new ConstructableFileSystemCreateEvent( | |
| 189 event.path, event.isDirectory)); | |
| 190 } | |
| 191 | |
| 192 return eventsForPaths; | 177 return eventsForPaths; |
| 193 } | 178 } |
| 194 | 179 |
| 195 /// Returns the canonical event from a batch of events on the same path, if | 180 /// Returns the canonical event from a batch of events on the same path, if |
| 196 /// one exists. | 181 /// one exists. |
| 197 /// | 182 /// |
| 198 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and | 183 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and |
| 199 /// CREATE, or events with different values for [isDirectory]), this returns a | 184 /// CREATE, or events with different values for [isDirectory]), this returns a |
| 200 /// single event that describes what happened to the path in question. | 185 /// single event that describes what happened to the path in question. |
| 201 /// | 186 /// |
| 202 /// If [batch] does contain contradictory events, this returns `null` to | 187 /// If [batch] does contain contradictory events, this returns `null` to |
| 203 /// indicate that the state of the path on the filesystem should be checked to | 188 /// indicate that the state of the path on the filesystem should be checked to |
| 204 /// determine what occurred. | 189 /// determine what occurred. |
| 205 FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) { | 190 FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) { |
| 206 // An empty batch indicates that we've learned earlier that the batch is | 191 // An empty batch indicates that we've learned earlier that the batch is |
| 207 // contradictory (e.g. because of a move). | 192 // contradictory (e.g. because of a move). |
| 208 if (batch.isEmpty) return null; | 193 if (batch.isEmpty) return null; |
| 209 | 194 |
| 210 var type = batch.first.type; | 195 var type = batch.first.type; |
| 211 var isDir = batch.first.isDirectory; | 196 var isDir = batch.first.isDirectory; |
| 197 var hadModifyEvent = false; |
| 212 | 198 |
| 213 for (var event in batch.skip(1)) { | 199 for (var event in batch.skip(1)) { |
| 214 // If one event reports that the file is a directory and another event | 200 // If one event reports that the file is a directory and another event |
| 215 // doesn't, that's a contradiction. | 201 // doesn't, that's a contradiction. |
| 216 if (isDir != event.isDirectory) return null; | 202 if (isDir != event.isDirectory) return null; |
| 217 | 203 |
| 218 // Modify events don't contradict either CREATE or REMOVE events. We can | 204 // Modify events don't contradict either CREATE or REMOVE events. We can |
| 219 // safely assume the file was modified after a CREATE or before the | 205 // safely assume the file was modified after a CREATE or before the |
| 220 // REMOVE; otherwise there will also be a REMOVE or CREATE event | 206 // REMOVE; otherwise there will also be a REMOVE or CREATE event |
| 221 // (respectively) that will be contradictory. | 207 // (respectively) that will be contradictory. |
| 222 if (event is FileSystemModifyEvent) continue; | 208 if (event is FileSystemModifyEvent) { |
| 209 hadModifyEvent = true; |
| 210 continue; |
| 211 } |
| 223 assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent); | 212 assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent); |
| 224 | 213 |
| 225 // If we previously thought this was a MODIFY, we now consider it to be a | 214 // If we previously thought this was a MODIFY, we now consider it to be a |
| 226 // CREATE or REMOVE event. This is safe for the same reason as above. | 215 // CREATE or REMOVE event. This is safe for the same reason as above. |
| 227 if (type == FileSystemEvent.MODIFY) { | 216 if (type == FileSystemEvent.MODIFY) { |
| 228 type = event.type; | 217 type = event.type; |
| 229 continue; | 218 continue; |
| 230 } | 219 } |
| 231 | 220 |
| 232 // A CREATE event contradicts a REMOVE event and vice versa. | 221 // A CREATE event contradicts a REMOVE event and vice versa. |
| 233 assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE); | 222 assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE); |
| 234 if (type != event.type) return null; | 223 if (type != event.type) return null; |
| 235 } | 224 } |
| 236 | 225 |
| 226 // If we got a CREATE event for a file we already knew about, that comes |
| 227 // from FSEvents reporting an add that happened prior to the watch |
| 228 // beginning. If we also received a MODIFY event, we want to report that, |
| 229 // but not the CREATE. |
| 230 if (type == FileSystemEvent.CREATE && hadModifyEvent && |
| 231 _files.contains(batch.first.path)) { |
| 232 type = FileSystemEvent.MODIFY; |
| 233 } |
| 234 |
| 237 switch (type) { | 235 switch (type) { |
| 238 case FileSystemEvent.CREATE: | 236 case FileSystemEvent.CREATE: |
| 239 // Issue 14793 means that CREATE events can actually mean DELETE, so we | 237 return new ConstructableFileSystemCreateEvent(batch.first.path, isDir); |
| 240 // should always check the filesystem for them. | |
| 241 return null; | |
| 242 case FileSystemEvent.DELETE: | 238 case FileSystemEvent.DELETE: |
| 243 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir); | 239 // Issue 15458 means that DELETE events for directories can actually |
| 240 // mean CREATE, so we always check the filesystem for them. |
| 241 if (isDir) return null; |
| 242 return new ConstructableFileSystemCreateEvent(batch.first.path, false); |
| 244 case FileSystemEvent.MODIFY: | 243 case FileSystemEvent.MODIFY: |
| 245 return new ConstructableFileSystemModifyEvent( | 244 return new ConstructableFileSystemModifyEvent( |
| 246 batch.first.path, isDir, false); | 245 batch.first.path, isDir, false); |
| 247 default: assert(false); | 246 default: assert(false); |
| 248 } | 247 } |
| 249 } | 248 } |
| 250 | 249 |
| 251 /// Returns one or more events that describe the change between the last known | 250 /// Returns one or more events that describe the change between the last known |
| 252 /// state of [path] and its current state on the filesystem. | 251 /// state of [path] and its current state on the filesystem. |
| 253 /// | 252 /// |
| (...skipping 64 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 318 new BatchedStreamTransformer<FileSystemEvent>()); | 317 new BatchedStreamTransformer<FileSystemEvent>()); |
| 319 _watchSubscription = innerStream.listen(_onBatch, | 318 _watchSubscription = innerStream.listen(_onBatch, |
| 320 onError: _eventsController.addError, | 319 onError: _eventsController.addError, |
| 321 onDone: _onDone); | 320 onDone: _onDone); |
| 322 } | 321 } |
| 323 | 322 |
| 324 /// Emit an event with the given [type] and [path]. | 323 /// Emit an event with the given [type] and [path]. |
| 325 void _emitEvent(ChangeType type, String path) { | 324 void _emitEvent(ChangeType type, String path) { |
| 326 if (!isReady) return; | 325 if (!isReady) return; |
| 327 | 326 |
| 328 // Don't emit ADD events for files that we already know about. Such an event | |
| 329 // probably comes from FSEvents reporting an add that happened prior to the | |
| 330 // watch beginning. | |
| 331 if (type == ChangeType.ADD && _files.contains(path)) return; | |
| 332 | |
| 333 _eventsController.add(new WatchEvent(type, path)); | 327 _eventsController.add(new WatchEvent(type, path)); |
| 334 } | 328 } |
| 335 | 329 |
| 336 /// Emit an error, then close the watcher. | 330 /// Emit an error, then close the watcher. |
| 337 void _emitError(error, StackTrace stackTrace) { | 331 void _emitError(error, StackTrace stackTrace) { |
| 338 _eventsController.addError(error, stackTrace); | 332 _eventsController.addError(error, stackTrace); |
| 339 close(); | 333 close(); |
| 340 } | 334 } |
| 341 | 335 |
| 342 /// Like [Stream.listen], but automatically adds the subscription to | 336 /// Like [Stream.listen], but automatically adds the subscription to |
| 343 /// [_subscriptions] so that it can be canceled when [close] is called. | 337 /// [_subscriptions] so that it can be canceled when [close] is called. |
| 344 void _listen(Stream stream, void onData(event), {Function onError, | 338 void _listen(Stream stream, void onData(event), {Function onError, |
| 345 void onDone(), bool cancelOnError}) { | 339 void onDone(), bool cancelOnError}) { |
| 346 var subscription; | 340 var subscription; |
| 347 subscription = stream.listen(onData, onError: onError, onDone: () { | 341 subscription = stream.listen(onData, onError: onError, onDone: () { |
| 348 _subscriptions.remove(subscription); | 342 _subscriptions.remove(subscription); |
| 349 if (onDone != null) onDone(); | 343 if (onDone != null) onDone(); |
| 350 }, cancelOnError: cancelOnError); | 344 }, cancelOnError: cancelOnError); |
| 351 _subscriptions.add(subscription); | 345 _subscriptions.add(subscription); |
| 352 } | 346 } |
| 353 } | 347 } |
| OLD | NEW |