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; |
| 11 |
10 import '../constructable_file_system_event.dart'; | 12 import '../constructable_file_system_event.dart'; |
11 import '../path_set.dart'; | 13 import '../path_set.dart'; |
12 import '../utils.dart'; | 14 import '../utils.dart'; |
13 import '../watch_event.dart'; | 15 import '../watch_event.dart'; |
14 import 'resubscribable.dart'; | 16 import 'resubscribable.dart'; |
15 | 17 |
16 /// Uses the FSEvents subsystem to watch for filesystem events. | 18 /// Uses the FSEvents subsystem to watch for filesystem events. |
17 /// | 19 /// |
18 /// FSEvents has two main idiosyncrasies that this class works around. First, it | 20 /// FSEvents has two main idiosyncrasies that this class works around. First, it |
19 /// will occasionally report events that occurred before the filesystem watch | 21 /// will occasionally report events that occurred before the filesystem watch |
20 /// was initiated. Second, if multiple events happen to the same file in close | 22 /// 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 | 23 /// succession, it won't report them in the order they occurred. See issue |
22 /// 14373. | 24 /// 14373. |
23 /// | 25 /// |
24 /// This also works around issues 14793, 14806, and 14849 in the implementation | 26 /// This also works around issues 14793, 14806, and 14849 in the implementation |
25 /// of [Directory.watch]. | 27 /// of [Directory.watch]. |
26 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher { | 28 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher { |
| 29 // TODO(nweiz): remove this when issue 15042 is fixed. |
| 30 static bool logDebugInfo = false; |
| 31 |
27 MacOSDirectoryWatcher(String directory) | 32 MacOSDirectoryWatcher(String directory) |
28 : super(directory, () => new _MacOSDirectoryWatcher(directory)); | 33 : super(directory, () => new _MacOSDirectoryWatcher(directory)); |
29 } | 34 } |
30 | 35 |
31 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher { | 36 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher { |
32 final String directory; | 37 final String directory; |
33 | 38 |
34 Stream<WatchEvent> get events => _eventsController.stream; | 39 Stream<WatchEvent> get events => _eventsController.stream; |
35 final _eventsController = new StreamController<WatchEvent>.broadcast(); | 40 final _eventsController = new StreamController<WatchEvent>.broadcast(); |
36 | 41 |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
71 _MacOSDirectoryWatcher(String directory) | 76 _MacOSDirectoryWatcher(String directory) |
72 : directory = directory, | 77 : directory = directory, |
73 _files = new PathSet(directory) { | 78 _files = new PathSet(directory) { |
74 _startWatch(); | 79 _startWatch(); |
75 | 80 |
76 _listen(new Directory(directory).list(recursive: true), | 81 _listen(new Directory(directory).list(recursive: true), |
77 (entity) { | 82 (entity) { |
78 if (entity is! Directory) _files.add(entity.path); | 83 if (entity is! Directory) _files.add(entity.path); |
79 }, | 84 }, |
80 onError: _emitError, | 85 onError: _emitError, |
81 onDone: _readyCompleter.complete, | 86 onDone: () { |
| 87 if (MacOSDirectoryWatcher.logDebugInfo) { |
| 88 print("watcher is ready"); |
| 89 } |
| 90 _readyCompleter.complete(); |
| 91 }, |
82 cancelOnError: true); | 92 cancelOnError: true); |
83 } | 93 } |
84 | 94 |
85 void close() { | 95 void close() { |
86 for (var subscription in _subscriptions) { | 96 for (var subscription in _subscriptions) { |
87 subscription.cancel(); | 97 subscription.cancel(); |
88 } | 98 } |
89 _subscriptions.clear(); | 99 _subscriptions.clear(); |
90 if (_watchSubscription != null) _watchSubscription.cancel(); | 100 if (_watchSubscription != null) _watchSubscription.cancel(); |
91 _watchSubscription = null; | 101 _watchSubscription = null; |
92 _eventsController.close(); | 102 _eventsController.close(); |
93 } | 103 } |
94 | 104 |
95 /// The callback that's run when [Directory.watch] emits a batch of events. | 105 /// The callback that's run when [Directory.watch] emits a batch of events. |
96 void _onBatch(List<FileSystemEvent> batch) { | 106 void _onBatch(List<FileSystemEvent> batch) { |
| 107 if (MacOSDirectoryWatcher.logDebugInfo) { |
| 108 print("======== batch:"); |
| 109 for (var event in batch) { |
| 110 print(" ${_formatEvent(event)}"); |
| 111 } |
| 112 |
| 113 print("known files:"); |
| 114 for (var foo in _files.toSet()) { |
| 115 print(" ${p.relative(foo, from: directory)}"); |
| 116 } |
| 117 } |
| 118 |
97 batches++; | 119 batches++; |
98 | 120 |
99 _sortEvents(batch).forEach((path, events) { | 121 _sortEvents(batch).forEach((path, events) { |
| 122 var relativePath = p.relative(path, from: directory); |
| 123 if (MacOSDirectoryWatcher.logDebugInfo) { |
| 124 print("events for $relativePath:\n"); |
| 125 for (var event in events) { |
| 126 print(" ${_formatEvent(event)}"); |
| 127 } |
| 128 } |
| 129 |
100 var canonicalEvent = _canonicalEvent(events); | 130 var canonicalEvent = _canonicalEvent(events); |
101 events = canonicalEvent == null ? | 131 events = canonicalEvent == null ? |
102 _eventsBasedOnFileSystem(path) : [canonicalEvent]; | 132 _eventsBasedOnFileSystem(path) : [canonicalEvent]; |
| 133 if (MacOSDirectoryWatcher.logDebugInfo) { |
| 134 print("canonical event for $relativePath: " |
| 135 "${_formatEvent(canonicalEvent)}"); |
| 136 print("actionable events for $relativePath: " |
| 137 "${events.map(_formatEvent)}"); |
| 138 } |
103 | 139 |
104 for (var event in events) { | 140 for (var event in events) { |
105 if (event is FileSystemCreateEvent) { | 141 if (event is FileSystemCreateEvent) { |
106 if (!event.isDirectory) { | 142 if (!event.isDirectory) { |
107 _emitEvent(ChangeType.ADD, path); | 143 _emitEvent(ChangeType.ADD, path); |
108 _files.add(path); | 144 _files.add(path); |
109 continue; | 145 continue; |
110 } | 146 } |
111 | 147 |
112 _listen(new Directory(path).list(recursive: true), (entity) { | 148 _listen(new Directory(path).list(recursive: true), (entity) { |
113 if (entity is Directory) return; | 149 if (entity is Directory) return; |
114 _emitEvent(ChangeType.ADD, entity.path); | 150 _emitEvent(ChangeType.ADD, entity.path); |
115 _files.add(entity.path); | 151 _files.add(entity.path); |
116 }, onError: _emitError, cancelOnError: true); | 152 }, onError: (e, stackTrace) { |
| 153 if (MacOSDirectoryWatcher.logDebugInfo) { |
| 154 print("got error listing $relativePath: $e"); |
| 155 } |
| 156 _emitError(e, stackTrace); |
| 157 }, cancelOnError: true); |
117 } else if (event is FileSystemModifyEvent) { | 158 } else if (event is FileSystemModifyEvent) { |
118 assert(!event.isDirectory); | 159 assert(!event.isDirectory); |
119 _emitEvent(ChangeType.MODIFY, path); | 160 _emitEvent(ChangeType.MODIFY, path); |
120 } else { | 161 } else { |
121 assert(event is FileSystemDeleteEvent); | 162 assert(event is FileSystemDeleteEvent); |
122 for (var removedPath in _files.remove(path)) { | 163 for (var removedPath in _files.remove(path)) { |
123 _emitEvent(ChangeType.REMOVE, removedPath); | 164 _emitEvent(ChangeType.REMOVE, removedPath); |
124 } | 165 } |
125 } | 166 } |
126 } | 167 } |
127 }); | 168 }); |
| 169 |
| 170 if (MacOSDirectoryWatcher.logDebugInfo) { |
| 171 print("========"); |
| 172 } |
128 } | 173 } |
129 | 174 |
130 /// Sort all the events in a batch into sets based on their path. | 175 /// Sort all the events in a batch into sets based on their path. |
131 /// | 176 /// |
132 /// A single input event may result in multiple events in the returned map; | 177 /// A single input event may result in multiple events in the returned map; |
133 /// for example, a MOVE event becomes a DELETE event for the source and a | 178 /// for example, a MOVE event becomes a DELETE event for the source and a |
134 /// CREATE event for the destination. | 179 /// CREATE event for the destination. |
135 /// | 180 /// |
136 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it | 181 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it |
137 /// contain any events relating to [directory]. | 182 /// contain any events relating to [directory]. |
(...skipping 116 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
254 /// This returns a list whose order should be reflected in the events emitted | 299 /// This returns a list whose order should be reflected in the events emitted |
255 /// to the user, unlike the batched events from [Directory.watch]. The | 300 /// to the user, unlike the batched events from [Directory.watch]. The |
256 /// returned list may be empty, indicating that no changes occurred to [path] | 301 /// returned list may be empty, indicating that no changes occurred to [path] |
257 /// (probably indicating that it was created and then immediately deleted). | 302 /// (probably indicating that it was created and then immediately deleted). |
258 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) { | 303 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) { |
259 var fileExisted = _files.contains(path); | 304 var fileExisted = _files.contains(path); |
260 var dirExisted = _files.containsDir(path); | 305 var dirExisted = _files.containsDir(path); |
261 var fileExists = new File(path).existsSync(); | 306 var fileExists = new File(path).existsSync(); |
262 var dirExists = new Directory(path).existsSync(); | 307 var dirExists = new Directory(path).existsSync(); |
263 | 308 |
| 309 if (MacOSDirectoryWatcher.logDebugInfo) { |
| 310 print("file existed: $fileExisted"); |
| 311 print("dir existed: $dirExisted"); |
| 312 print("file exists: $fileExists"); |
| 313 print("dir exists: $dirExists"); |
| 314 } |
| 315 |
264 var events = []; | 316 var events = []; |
265 if (fileExisted) { | 317 if (fileExisted) { |
266 if (fileExists) { | 318 if (fileExists) { |
267 events.add(new ConstructableFileSystemModifyEvent(path, false, false)); | 319 events.add(new ConstructableFileSystemModifyEvent(path, false, false)); |
268 } else { | 320 } else { |
269 events.add(new ConstructableFileSystemDeleteEvent(path, false)); | 321 events.add(new ConstructableFileSystemDeleteEvent(path, false)); |
270 } | 322 } |
271 } else if (dirExisted) { | 323 } else if (dirExisted) { |
272 if (dirExists) { | 324 if (dirExists) { |
273 // If we got contradictory events for a directory that used to exist and | 325 // If we got contradictory events for a directory that used to exist and |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
323 | 375 |
324 /// Emit an event with the given [type] and [path]. | 376 /// Emit an event with the given [type] and [path]. |
325 void _emitEvent(ChangeType type, String path) { | 377 void _emitEvent(ChangeType type, String path) { |
326 if (!isReady) return; | 378 if (!isReady) return; |
327 | 379 |
328 // Don't emit ADD events for files that we already know about. Such an event | 380 // 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 | 381 // probably comes from FSEvents reporting an add that happened prior to the |
330 // watch beginning. | 382 // watch beginning. |
331 if (type == ChangeType.ADD && _files.contains(path)) return; | 383 if (type == ChangeType.ADD && _files.contains(path)) return; |
332 | 384 |
| 385 if (MacOSDirectoryWatcher.logDebugInfo) { |
| 386 print("emitting $type ${p.relative(path, from: directory)}"); |
| 387 } |
| 388 |
333 _eventsController.add(new WatchEvent(type, path)); | 389 _eventsController.add(new WatchEvent(type, path)); |
334 } | 390 } |
335 | 391 |
336 /// Emit an error, then close the watcher. | 392 /// Emit an error, then close the watcher. |
337 void _emitError(error, StackTrace stackTrace) { | 393 void _emitError(error, StackTrace stackTrace) { |
338 _eventsController.addError(error, stackTrace); | 394 _eventsController.addError(error, stackTrace); |
339 close(); | 395 close(); |
340 } | 396 } |
341 | 397 |
342 /// Like [Stream.listen], but automatically adds the subscription to | 398 /// Like [Stream.listen], but automatically adds the subscription to |
343 /// [_subscriptions] so that it can be canceled when [close] is called. | 399 /// [_subscriptions] so that it can be canceled when [close] is called. |
344 void _listen(Stream stream, void onData(event), {Function onError, | 400 void _listen(Stream stream, void onData(event), {Function onError, |
345 void onDone(), bool cancelOnError}) { | 401 void onDone(), bool cancelOnError}) { |
346 var subscription; | 402 var subscription; |
347 subscription = stream.listen(onData, onError: onError, onDone: () { | 403 subscription = stream.listen(onData, onError: onError, onDone: () { |
348 _subscriptions.remove(subscription); | 404 _subscriptions.remove(subscription); |
349 if (onDone != null) onDone(); | 405 if (onDone != null) onDone(); |
350 }, cancelOnError: cancelOnError); | 406 }, cancelOnError: cancelOnError); |
351 _subscriptions.add(subscription); | 407 _subscriptions.add(subscription); |
352 } | 408 } |
| 409 |
| 410 // TODO(nweiz): remove this when issue 15042 is fixed. |
| 411 /// Return a human-friendly string representation of [event]. |
| 412 String _formatEvent(FileSystemEvent event) { |
| 413 if (event == null) return 'null'; |
| 414 |
| 415 var path = p.relative(event.path, from: directory); |
| 416 var type = event.isDirectory ? 'directory' : 'file'; |
| 417 if (event is FileSystemCreateEvent) { |
| 418 return "create $type $path"; |
| 419 } else if (event is FileSystemDeleteEvent) { |
| 420 return "delete $type $path"; |
| 421 } else if (event is FileSystemModifyEvent) { |
| 422 return "modify $type $path"; |
| 423 } else if (event is FileSystemMoveEvent) { |
| 424 return "move $type $path to " |
| 425 "${p.relative(event.destination, from: directory)}"; |
| 426 } |
| 427 } |
353 } | 428 } |
OLD | NEW |