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