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 |