Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(241)

Side by Side Diff: pkg/watcher/lib/src/directory_watcher/mac_os.dart

Issue 66163002: Wrap Directory.watch on Mac OS for the watcher package. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 '../constructable_file_system_event.dart';
11 import '../path_set.dart';
12 import '../utils.dart';
13 import '../watch_event.dart';
14 import 'resubscribable.dart';
15
16 import 'package:path/path.dart' as p;
17
18 /// Uses the FSEvents subsystem to watch for filesystem events.
19 ///
20 /// FSEvents has two main idiosynchrasies that this class works around. First,
Bob Nystrom 2013/11/09 00:42:57 "idiosyncrasies"
nweiz 2013/11/09 02:19:26 Ten Signs You've Been Writing The Word "Asynchrono
21 /// it 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 /// succession, it won't report them in the order they occurred. See issue
24 /// 14373.
25 ///
26 /// This also works around issues 14793, 14806, and 14849 in the implementation
27 /// of [Directory.watch].
28 class MacOSDirectoryWatcher extends ResubscribableDirectoryWatcher {
29 MacOSDirectoryWatcher(String directory)
30 : super(directory, () => new _MacOSDirectoryWatcher(directory));
31 }
32
33 class _MacOSDirectoryWatcher implements ManuallyClosedDirectoryWatcher {
34 final String directory;
35
36 Stream<WatchEvent> get events => _eventsController.stream;
37 final _eventsController = new StreamController<WatchEvent>.broadcast();
38
39 bool get isReady => _readyCompleter.isCompleted;
40
41 Future get ready => _readyCompleter.future;
42 final _readyCompleter = new Completer();
43
44 /// The number of event batches that have been received from
45 /// [Directory.watch].
46 ///
47 /// This is used to determine if the [Directory.watch] stream was falsely
48 /// closed due to issue 14849. A close caused by events in the past will only
49 /// happen before or immediately after the first batch of events.
50 int batches = 0;
51
52 /// The set of files that are known to exist recursively within the watched
53 /// directory.
54 ///
55 /// The state of files on the filesystem is compared against this to determine
56 /// the real change that occurred when working around issue 14373. This is
57 /// also used to emit REMOVE events when subdirectories are moved out of the
58 /// watched directory.
59 final PathSet _files;
60
61 /// The subscription to the stream returned by [Directory.watch].
62 ///
63 /// This is separate from [_subscriptions] because this stream occasionally
64 /// needs to be resubscribed in order to work around issue 14849.
65 StreamSubscription<FileSystemEvent> _watchSubscription;
66
67 /// A set of subscriptions that this watcher subscribes to.
68 ///
69 /// These are gathered together so that they may all be canceled when the
70 /// watcher is closed. This does not include [_watchSubscription].
71 final _subscriptions = new Set<StreamSubscription>();
72
73 _MacOSDirectoryWatcher(String directory)
74 : directory = directory,
75 _files = new PathSet(directory) {
76 _startWatch();
77
78 _listen(new Directory(directory).list(recursive: true),
79 (entity) {
80 if (entity is! Directory) _files.add(entity.path);
81 },
82 onError: _emitError,
83 onDone: _readyCompleter.complete,
84 cancelOnError: true);
85 }
86
87 void close() {
88 for (var subscription in _subscriptions) {
89 subscription.cancel();
90 }
91 _subscriptions.clear();
92 if (_watchSubscription != null) _watchSubscription.cancel();
93 _watchSubscription = null;
94 _eventsController.close();
95 }
96
97 /// The callback that's run when [Directory.watch] emits a batch of events.
98 void _onBatch(List<FileSystemEvent> batch) {
99 batches++;
100
101 _sortEvents(batch).forEach((path, events) {
102 var canonicalEvent = _canonicalEvent(events);
103 events = canonicalEvent == null ? [] : [canonicalEvent];
104 if (events.isEmpty) events = _eventsBasedOnFileSystem(path);
Bob Nystrom 2013/11/09 00:42:57 Why set to an empty list above just to change it s
nweiz 2013/11/09 02:19:26 Good point. Changed.
105
106 for (var event in events) {
107 if (event is FileSystemCreateEvent) {
108 if (!event.isDirectory) {
109 _emitEvent(ChangeType.ADD, path);
110 _files.add(path);
111 continue;
112 }
113
114 _listen(new Directory(path).list(recursive: true), (entity) {
115 if (entity is Directory) return;
116 _emitEvent(ChangeType.ADD, entity.path);
117 _files.add(entity.path);
118 }, onError: _emitError, cancelOnError: true);
119 } else if (event is FileSystemModifyEvent) {
120 assert(!event.isDirectory);
121 _emitEvent(ChangeType.MODIFY, path);
122 } else {
123 assert(event is FileSystemDeleteEvent);
124 for (var removedPath in _files.remove(path)) {
125 _emitEvent(ChangeType.REMOVE, removedPath);
126 }
127 }
128 }
129 });
130 }
131
132 /// Sort all the events in a batch into sets based on their path.
133 ///
134 /// A single input event may result in multiple events in the returned map;
135 /// for example, a MOVE event becomes a DELETE event for the source and a
136 /// CREATE event for the destination.
137 ///
138 /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
139 /// contain any events relating to [directory].
Bob Nystrom 2013/11/09 00:42:57 "[directory]" is not a parameter here.
nweiz 2013/11/09 02:19:26 It is a field on the class, though.
140 Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
141 var eventsForPaths = {};
142
143 // FSEvents can report past events, including events on the root directory
144 // such as it being created. We want to ignore these. If the directory is
145 // really deleted, that's handled by [_onDone].
146 batch = batch.where((event) => event.path != directory).toList();
Bob Nystrom 2013/11/09 00:42:57 Is the .toList() needed here?
nweiz 2013/11/09 02:19:26 Since Iterables are lazy, the [where] will be exec
147
148 // Events within directories that already have events are superfluous; the
149 // directory's full contents will be examined anyway, so we ignore such
150 // events. Emitting them could cause useless or out-of-order events.
151 var directories = unionAll(batch.map((event) {
152 if (!event.isDirectory) return new Set();
153 if (event is! FileSystemMoveEvent) return new Set.from([event.path]);
154 return new Set.from([event.path, event.destination]);
155 }));
156 isInModifiedDirectory(path) =>
Bob Nystrom 2013/11/09 00:42:57 Nit: blank line above this please.
nweiz 2013/11/09 02:19:26 Done.
157 directories.any((dir) => path != dir && path.startsWith(dir));
158
159 addEvent(path, event) {
160 if (isInModifiedDirectory(path)) return;
161 var set = eventsForPaths.putIfAbsent(path, () => new Set());
162 set.add(event);
163 }
164
165 for (var event in batch.where((event) => event is! FileSystemMoveEvent)) {
166 addEvent(event.path, event);
167 }
168
169 // Issue 14806 means that move events can be misleading if they're in the
170 // same batch as another modification of a related file. If they are, we
171 // make the event set empty to ensure we check the state of the filesystem.
172 // Otherwise, treat them as a DELETE followed by an ADD.
173 for (var event in batch.where((event) => event is FileSystemMoveEvent)) {
174 if (eventsForPaths.containsKey(event.path) ||
175 eventsForPaths.containsKey(event.destination)) {
176
177 if (!isInModifiedDirectory(event.path)) {
178 eventsForPaths[event.path] = new Set();
179 }
180 if (!isInModifiedDirectory(event.destination)) {
181 eventsForPaths[event.destination] = new Set();
182 }
183
184 continue;
185 }
186
187 addEvent(event.path, new ConstructableFileSystemDeleteEvent(
188 event.path, event.isDirectory));
189 addEvent(event.destination, new ConstructableFileSystemCreateEvent(
190 event.path, event.isDirectory));
191 }
192
193 return eventsForPaths;
194 }
195
196 /// Returns the canonical event from a batch of events on the same path, if
197 /// one exists.
198 ///
199 /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
200 /// CREATE, or events with different values for [isDirectory]), this returns a
201 /// single event that describes that happened to the path in question.
Bob Nystrom 2013/11/09 00:42:57 "that happened" -> "what happened".
nweiz 2013/11/09 02:19:26 Done.
202 ///
203 /// If [batch] does contain contradictory events, this returns `null` to
204 /// indicate that the state of the path on the filesystem should be checked to
205 /// determine what occurred.
206 FileSystemEvent _canonicalEvent(Set<FileSystemEvent> batch) {
207 // An empty batch indicates that we've learned earlier that the batch is
208 // contradictory (e.g. because of a move).
209 if (batch.isEmpty) return null;
210
211 var type = batch.first.type;
212 var isDir = batch.first.isDirectory;
213
214 for (var event in batch.skip(1)) {
215 // If one event reports that the file is a directory and another event
216 // doesn't, that's a contradiction so we should return null.
Bob Nystrom 2013/11/09 00:42:57 Remove "so we should return null".
nweiz 2013/11/09 02:19:26 Done.
nweiz 2013/11/09 02:19:26 Done.
217 if (isDir != event.isDirectory) return null;
218
219 // Modify events don't contradict either CREATE or REMOVE events. We can
220 // safely assume the file was modified after a CREATE or before the
221 // REMOVE; otherwise there will also be a REMOVE or CREATE event
222 // (respectively) that will be contradictory.
223 if (event is FileSystemModifyEvent) continue;
224 assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
225
226 // If we previously thought this was a MODIFY, we now consider it to be a
227 // CREATE or REMOVE event. This is safe for the same reason as above.
228 if (type == FileSystemEvent.MODIFY) {
229 type = event.type;
230 continue;
231 }
232
233 // A CREATE event contradicts a REMOVE event and vice versa.
234 assert(type == FileSystemEvent.CREATE || type == FileSystemEvent.DELETE);
235 if (type != event.type) return null;
236 }
237
238 switch (type) {
239 case FileSystemEvent.CREATE:
240 // Issue 14793 means that CREATE events can actually mean DELETE, so we
241 // should always check the filesystem for them.
242 return null;
243 case FileSystemEvent.DELETE:
244 return new ConstructableFileSystemDeleteEvent(batch.first.path, isDir);
245 case FileSystemEvent.MODIFY:
246 return new ConstructableFileSystemModifyEvent(
247 batch.first.path, isDir, false);
248 default: assert(false);
249 }
250 }
251
252 /// Returns one or more events that describe the change between the last known
253 /// state of [path] and its current state on the filesystem.
254 ///
255 /// This returns a list whose order should be reflected in the events emitted
256 /// to the user, unlike the batched events from [Directory.watch]. The
257 /// returned list may be empty, indicating that no changes occurred to [path]
258 /// (probably indicating that it was created and then immediately deleted).
259 List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
260 var fileExisted = _files.contains(path);
261 var dirExisted = _files.containsDir(path);
262 var fileExists = new File(path).existsSync();
263 var dirExists = new Directory(path).existsSync();
264
265 var events = [];
266 if (fileExisted) {
267 if (fileExists) {
268 events.add(new ConstructableFileSystemModifyEvent(path, false, false));
269 } else {
270 events.add(new ConstructableFileSystemDeleteEvent(path, false));
271 }
272 } else if (dirExisted) {
273 if (dirExists) {
274 // If we got contradictory events for a directory that used to exist and
275 // still exists, we need to rescan the whole thing in case it was
276 // replaced with a different directory.
277 events.add(new ConstructableFileSystemDeleteEvent(path, true));
278 events.add(new ConstructableFileSystemCreateEvent(path, true));
279 } else {
280 events.add(new ConstructableFileSystemDeleteEvent(path, true));
281 }
282 }
283
284 if (!fileExisted && fileExists) {
285 events.add(new ConstructableFileSystemCreateEvent(path, false));
286 } else if (!dirExisted && dirExists) {
287 events.add(new ConstructableFileSystemCreateEvent(path, true));
288 }
289
290 return events;
291 }
292
293 /// The callback that's run when the [Directory.watch] stream is closed.
294 void _onDone() {
295 _watchSubscription = null;
296
297 // If the directory still exists and we haven't seen more than one batch,
298 // this is probably issue 14849 rather than a real close event. We should
299 // just restart the watcher.
300 if (batches < 2 && new Directory(directory).existsSync()) {
301 _startWatch();
302 return;
303 }
304
305 // FSEvents can fail to report the contents of the directory being removed
306 // when the directory itself is removed, so we need to manually mark the as
307 // removed.
308 for (var file in _files.toSet()) {
309 _emitEvent(ChangeType.REMOVE, file);
310 }
311 _files.clear();
312 close();
313 }
314
315 /// Start or restart the underlying [Directory.watch] stream.
316 void _startWatch() {
317 // Batch the FSEvent changes together so that we can dedup events.
318 var innerStream = new Directory(directory).watch(recursive: true).transform(
319 new BatchedStreamTransformer<FileSystemEvent>());
320 _watchSubscription = innerStream.listen(_onBatch,
321 onError: _eventsController.addError,
322 onDone: _onDone);
323 }
324
325 /// Emit an event with the given [type] and [path].
326 void _emitEvent(ChangeType type, String path) {
327 if (!isReady) return;
328
329 // Don't emit ADD events for files that we already know about. Such an event
330 // probably comes from FSEvents reporting an add that happened prior to the
331 // watch beginning.
332 if (type == ChangeType.ADD && _files.contains(path)) return;
333
334 _eventsController.add(new WatchEvent(type, path));
335 }
336
337 /// Emit an error, then close the watcher.
338 void _emitError(error, StackTrace stackTrace) {
339 _eventsController.add(error, stackTrace);
340 close();
341 }
342
343 /// Like [Stream.listen], but automatically adds the subscription to
344 /// [_subscriptions] so that it can be canceled when [close] is called.
345 void _listen(Stream stream, void onData(event), {Function onError,
346 void onDone(), bool cancelOnError}) {
347 var subscription;
348 subscription = stream.listen(onData, onError: onError, onDone: () {
349 _subscriptions.remove(subscription);
350 if (onDone != null) onDone();
351 }, cancelOnError: cancelOnError);
352 _subscriptions.add(subscription);
353 }
354 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698