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