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 |