OLD | NEW |
| (Empty) |
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 | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library watcher.directory_watcher.mac_os; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:io'; | |
9 | |
10 import '../directory_watcher.dart'; | |
11 import '../constructable_file_system_event.dart'; | |
12 import '../path_set.dart'; | |
13 import '../resubscribable.dart'; | |
14 import '../utils.dart'; | |
15 import '../watch_event.dart'; | |
16 | |
17 /// Uses the FSEvents subsystem to watch for filesystem events. | |
18 /// | |
19 /// FSEvents has two main idiosyncrasies that this class works around. First, it | |
20 /// will occasionally report events that occurred before the filesystem watch | |
21 /// was initiated. Second, if multiple events happen to the same file in close | |
22 /// succession, it won't report them in the order they occurred. See issue | |
23 /// 14373. | |
24 /// | |
25 /// This also works around issues 16003 and 14849 in the implementation of | |
26 /// [Directory.watch]. | |
27 class MacOSDirectoryWatcher extends ResubscribableWatcher | |
28 implements DirectoryWatcher { | |
29 String get directory => path; | |
30 | |
31 MacOSDirectoryWatcher(String directory) | |
32 : super(directory, () => new _MacOSDirectoryWatcher(directory)); | |
33 } | |
34 | |
35 class _MacOSDirectoryWatcher | |
36 implements DirectoryWatcher, ManuallyClosedWatcher { | |
37 String get directory => path; | |
38 final String path; | |
39 | |
40 Stream<WatchEvent> get events => _eventsController.stream; | |
41 final _eventsController = new StreamController<WatchEvent>.broadcast(); | |
42 | |
43 bool get isReady => _readyCompleter.isCompleted; | |
44 | |
45 Future get ready => _readyCompleter.future; | |
46 final _readyCompleter = new Completer(); | |
47 | |
48 /// The set of files that are known to exist recursively within the watched | |
49 /// directory. | |
50 /// | |
51 /// The state of files on the filesystem is compared against this to determine | |
52 /// the real change that occurred when working around issue 14373. This is | |
53 /// also used to emit REMOVE events when subdirectories are moved out of the | |
54 /// watched directory. | |
55 final PathSet _files; | |
56 | |
57 /// The subscription to the stream returned by [Directory.watch]. | |
58 /// | |
59 /// This is separate from [_subscriptions] because this stream occasionally | |
60 /// needs to be resubscribed in order to work around issue 14849. | |
61 StreamSubscription<FileSystemEvent> _watchSubscription; | |
62 | |
63 /// The subscription to the [Directory.list] call for the initial listing of | |
64 /// the directory to determine its initial state. | |
65 StreamSubscription<FileSystemEntity> _initialListSubscription; | |
66 | |
67 /// The subscriptions to [Directory.list] calls for listing the contents of a | |
68 /// subdirectory that was moved into the watched directory. | |
69 final _listSubscriptions = new Set<StreamSubscription<FileSystemEntity>>(); | |
70 | |
71 /// The timer for tracking how long we wait for an initial batch of bogus | |
72 /// events (see issue 14373). | |
73 Timer _bogusEventTimer; | |
74 | |
75 _MacOSDirectoryWatcher(String path) | |
76 : path = path, | |
77 _files = new PathSet(path) { | |
78 _startWatch(); | |
79 | |
80 // Before we're ready to emit events, wait for [_listDir] to complete and | |
81 // for enough time to elapse that if bogus events (issue 14373) would be | |
82 // emitted, they will be. | |
83 // | |
84 // If we do receive a batch of events, [_onBatch] will ensure that these | |
85 // futures don't fire and that the directory is re-listed. | |
86 Future.wait([ | |
87 _listDir(), | |
88 _waitForBogusEvents() | |
89 ]).then((_) => _readyCompleter.complete()); | |
90 } | |
91 | |
92 void close() { | |
93 if (_watchSubscription != null) _watchSubscription.cancel(); | |
94 if (_initialListSubscription != null) _initialListSubscription.cancel(); | |
95 _watchSubscription = null; | |
96 _initialListSubscription = null; | |
97 | |
98 for (var subscription in _listSubscriptions) { | |
99 subscription.cancel(); | |
100 } | |
101 _listSubscriptions.clear(); | |
102 | |
103 _eventsController.close(); | |
104 } | |
105 | |
106 /// The callback that's run when [Directory.watch] emits a batch of events. | |
107 void _onBatch(List<FileSystemEvent> batch) { | |
108 // If we get a batch of events before we're ready to begin emitting events, | |
109 // it's probable that it's a batch of pre-watcher events (see issue 14373). | |
110 // Ignore those events and re-list the directory. | |
111 if (!isReady) { | |
112 // Cancel the timer because bogus events only occur in the first batch, so | |
113 // we can fire [ready] as soon as we're done listing the directory. | |
114 _bogusEventTimer.cancel(); | |
115 _listDir().then((_) => _readyCompleter.complete()); | |
116 return; | |
117 } | |
118 | |
119 _sortEvents(batch).forEach((path, events) { | |
120 var canonicalEvent = _canonicalEvent(events); | |
121 events = canonicalEvent == null ? | |
122 _eventsBasedOnFileSystem(path) : [canonicalEvent]; | |
123 | |
124 for (var event in events) { | |
125 if (event is FileSystemCreateEvent) { | |
126 if (!event.isDirectory) { | |
127 // If we already know about the file, treat it like a modification. | |
128 // This can happen if a file is copied on top of an existing one. | |
129 // We'll see an ADD event for the latter file when from the user's | |
130 // perspective, the file's contents just changed. | |
131 var type = _files.contains(path) | |
132 ? ChangeType.MODIFY | |
133 : ChangeType.ADD; | |
134 | |
135 _emitEvent(type, path); | |
136 _files.add(path); | |
137 continue; | |
138 } | |
139 | |
140 if (_files.containsDir(path)) continue; | |
141 | |
142 var subscription; | |
143 subscription = new Directory(path).list(recursive: true) | |
144 .listen((entity) { | |
145 if (entity is Directory) return; | |
146 if (_files.contains(path)) return; | |
147 | |
148 _emitEvent(ChangeType.ADD, entity.path); | |
149 _files.add(entity.path); | |
150 }, onError: (e, stackTrace) { | |
151 _emitError(e, stackTrace); | |
152 }, onDone: () { | |
153 _listSubscriptions.remove(subscription); | |
154 }, cancelOnError: true); | |
155 _listSubscriptions.add(subscription); | |
156 } else if (event is FileSystemModifyEvent) { | |
157 assert(!event.isDirectory); | |
158 _emitEvent(ChangeType.MODIFY, path); | |
159 } else { | |
160 assert(event is FileSystemDeleteEvent); | |
161 for (var removedPath in _files.remove(path)) { | |
162 _emitEvent(ChangeType.REMOVE, removedPath); | |
163 } | |
164 } | |
165 } | |
166 }); | |
167 } | |
168 | |
169 /// Sort all the events in a batch into sets based on their path. | |
170 /// | |
171 /// A single input event may result in multiple events in the returned map; | |
172 /// for example, a MOVE event becomes a DELETE event for the source and a | |
173 /// CREATE event for the destination. | |
174 /// | |
175 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it | |
176 /// contain any events relating to [path]. | |
177 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) { | |
178 var eventsForPaths = {}; | |
179 | |
180 // FSEvents can report past events, including events on the root directory | |
181 // such as it being created. We want to ignore these. If the directory is | |
182 // really deleted, that's handled by [_onDone]. | |
183 batch = batch.where((event) => event.path != path).toList(); | |
184 | |
185 // Events within directories that already have events are superfluous; the | |
186 // directory's full contents will be examined anyway, so we ignore such | |
187 // events. Emitting them could cause useless or out-of-order events. | |
188 var directories = unionAll(batch.map((event) { | |
189 if (!event.isDirectory) return new Set(); | |
190 if (event is! FileSystemMoveEvent) return new Set.from([event.path]); | |
191 return new Set.from([event.path, event.destination]); | |
192 })); | |
193 | |
194 isInModifiedDirectory(path) => | |
195 directories.any((dir) => path != dir && path.startsWith(dir)); | |
196 | |
197 addEvent(path, event) { | |
198 if (isInModifiedDirectory(path)) return; | |
199 var set = eventsForPaths.putIfAbsent(path, () => new Set()); | |
200 set.add(event); | |
201 } | |
202 | |
203 for (var event in batch) { | |
204 // The Mac OS watcher doesn't emit move events. See issue 14806. | |
205 assert(event is! FileSystemMoveEvent); | |
206 addEvent(event.path, event); | |
207 } | |
208 | |
209 return eventsForPaths; | |
210 } | |
211 | |
212 /// Returns the canonical event from a batch of events on the same path, if | |
213 /// one exists. | |
214 /// | |
215 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and | |
216 /// CREATE, or events with different values for [isDirectory]), this returns a | |
217 /// single event that describes what happened to the path in question. | |
218 /// | |
219 /// If [batch] does contain contradictory events, this returns `null` to | |
220 /// indicate that the state of the path on the filesystem should be checked to | |
221 /// determine what occurred. | |
222 FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) { | |
223 // An empty batch indicates that we've learned earlier that the batch is | |
224 // contradictory (e.g. because of a move). | |
225 if (batch.isEmpty) return null; | |
226 | |
227 var type = batch.first.type; | |
228 var isDir = batch.first.isDirectory; | |
229 var hadModifyEvent = false; | |
230 | |
231 for (var event in batch.skip(1)) { | |
232 // If one event reports that the file is a directory and another event | |
233 // doesn't, that's a contradiction. | |
234 if (isDir != event.isDirectory) return null; | |
235 | |
236 // Modify events don't contradict either CREATE or REMOVE events. We can | |
237 // safely assume the file was modified after a CREATE or before the | |
238 // REMOVE; otherwise there will also be a REMOVE or CREATE event | |
239 // (respectively) that will be contradictory. | |
240 if (event is FileSystemModifyEvent) { | |
241 hadModifyEvent = true; | |
242 continue; | |
243 } | |
244 assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent); | |
245 | |
246 // If we previously thought this was a MODIFY, we now consider it to be a | |
247 // CREATE or REMOVE event. This is safe for the same reason as above. | |
248 if (type == FileSystemEvent.MODIFY) { | |
249 type = event.type; | |
250 continue; | |
251 } | |
252 | |
253 // A CREATE event contradicts a REMOVE event and vice versa. | |
254 assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE); | |
255 if (type != event.type) return null; | |
256 } | |
257 | |
258 // If we got a CREATE event for a file we already knew about, that comes | |
259 // from FSEvents reporting an add that happened prior to the watch | |
260 // beginning. If we also received a MODIFY event, we want to report that, | |
261 // but not the CREATE. | |
262 if (type == FileSystemEvent.CREATE && hadModifyEvent && | |
263 _files.contains(batch.first.path)) { | |
264 type = FileSystemEvent.MODIFY; | |
265 } | |
266 | |
267 switch (type) { | |
268 case FileSystemEvent.CREATE: | |
269 // Issue 16003 means that a CREATE event for a directory can indicate | |
270 // that the directory was moved and then re-created. | |
271 // [_eventsBasedOnFileSystem] will handle this correctly by producing a | |
272 // DELETE event followed by a CREATE event if the directory exists. | |
273 if (isDir) return null; | |
274 return new ConstructableFileSystemCreateEvent(batch.first.path, false); | |
275 case FileSystemEvent.DELETE: | |
276 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir); | |
277 case FileSystemEvent.MODIFY: | |
278 return new ConstructableFileSystemModifyEvent( | |
279 batch.first.path, isDir, false); | |
280 default: throw 'unreachable'; | |
281 } | |
282 } | |
283 | |
284 /// Returns one or more events that describe the change between the last known | |
285 /// state of [path] and its current state on the filesystem. | |
286 /// | |
287 /// This returns a list whose order should be reflected in the events emitted | |
288 /// to the user, unlike the batched events from [Directory.watch]. The | |
289 /// returned list may be empty, indicating that no changes occurred to [path] | |
290 /// (probably indicating that it was created and then immediately deleted). | |
291 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) { | |
292 var fileExisted = _files.contains(path); | |
293 var dirExisted = _files.containsDir(path); | |
294 var fileExists = new File(path).existsSync(); | |
295 var dirExists = new Directory(path).existsSync(); | |
296 | |
297 var events = []; | |
298 if (fileExisted) { | |
299 if (fileExists) { | |
300 events.add(new ConstructableFileSystemModifyEvent(path, false, false)); | |
301 } else { | |
302 events.add(new ConstructableFileSystemDeleteEvent(path, false)); | |
303 } | |
304 } else if (dirExisted) { | |
305 if (dirExists) { | |
306 // If we got contradictory events for a directory that used to exist and | |
307 // still exists, we need to rescan the whole thing in case it was | |
308 // replaced with a different directory. | |
309 events.add(new ConstructableFileSystemDeleteEvent(path, true)); | |
310 events.add(new ConstructableFileSystemCreateEvent(path, true)); | |
311 } else { | |
312 events.add(new ConstructableFileSystemDeleteEvent(path, true)); | |
313 } | |
314 } | |
315 | |
316 if (!fileExisted && fileExists) { | |
317 events.add(new ConstructableFileSystemCreateEvent(path, false)); | |
318 } else if (!dirExisted && dirExists) { | |
319 events.add(new ConstructableFileSystemCreateEvent(path, true)); | |
320 } | |
321 | |
322 return events; | |
323 } | |
324 | |
325 /// The callback that's run when the [Directory.watch] stream is closed. | |
326 void _onDone() { | |
327 _watchSubscription = null; | |
328 | |
329 // If the directory still exists and we're still expecting bogus events, | |
330 // this is probably issue 14849 rather than a real close event. We should | |
331 // just restart the watcher. | |
332 if (!isReady && new Directory(path).existsSync()) { | |
333 _startWatch(); | |
334 return; | |
335 } | |
336 | |
337 // FSEvents can fail to report the contents of the directory being removed | |
338 // when the directory itself is removed, so we need to manually mark the | |
339 // files as removed. | |
340 for (var file in _files.toSet()) { | |
341 _emitEvent(ChangeType.REMOVE, file); | |
342 } | |
343 _files.clear(); | |
344 close(); | |
345 } | |
346 | |
347 /// Start or restart the underlying [Directory.watch] stream. | |
348 void _startWatch() { | |
349 // Batch the FSEvent changes together so that we can dedup events. | |
350 var innerStream = new Directory(path).watch(recursive: true) | |
351 .transform(new BatchedStreamTransformer<FileSystemEvent>()); | |
352 _watchSubscription = innerStream.listen(_onBatch, | |
353 onError: _eventsController.addError, | |
354 onDone: _onDone); | |
355 } | |
356 | |
357 /// Starts or restarts listing the watched directory to get an initial picture | |
358 /// of its state. | |
359 Future _listDir() { | |
360 assert(!isReady); | |
361 if (_initialListSubscription != null) _initialListSubscription.cancel(); | |
362 | |
363 _files.clear(); | |
364 var completer = new Completer(); | |
365 var stream = new Directory(path).list(recursive: true); | |
366 _initialListSubscription = stream.listen((entity) { | |
367 if (entity is! Directory) _files.add(entity.path); | |
368 }, | |
369 onError: _emitError, | |
370 onDone: completer.complete, | |
371 cancelOnError: true); | |
372 return completer.future; | |
373 } | |
374 | |
375 /// Wait 200ms for a batch of bogus events (issue 14373) to come in. | |
376 /// | |
377 /// 200ms is short in terms of human interaction, but longer than any Mac OS | |
378 /// watcher tests take on the bots, so it should be safe to assume that any | |
379 /// bogus events will be signaled in that time frame. | |
380 Future _waitForBogusEvents() { | |
381 var completer = new Completer(); | |
382 _bogusEventTimer = new Timer( | |
383 new Duration(milliseconds: 200), | |
384 completer.complete); | |
385 return completer.future; | |
386 } | |
387 | |
388 /// Emit an event with the given [type] and [path]. | |
389 void _emitEvent(ChangeType type, String path) { | |
390 if (!isReady) return; | |
391 _eventsController.add(new WatchEvent(type, path)); | |
392 } | |
393 | |
394 /// Emit an error, then close the watcher. | |
395 void _emitError(error, StackTrace stackTrace) { | |
396 _eventsController.addError(error, stackTrace); | |
397 close(); | |
398 } | |
399 } | |
OLD | NEW |